diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index c1032575ae4a6..49e36d2126cd4 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -6,13 +6,20 @@ const path = require('path'); const STORYBOOKS = [ 'apm', 'canvas', + 'codeeditor', 'ci_composite', 'url_template_editor', - 'codeeditor', 'dashboard', 'dashboard_enhanced', 'data_enhanced', 'embeddable', + 'expression_error', + 'expression_image', + 'expression_metric', + 'expression_repeat_image', + 'expression_reveal_image', + 'expression_shape', + 'expression_tagcloud', 'fleet', 'infra', 'security_solution', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 73d22362345bd..6ae834b58fc89 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -381,9 +381,9 @@ #CC# /x-pack/plugins/security_solution/ @elastic/security-solution # Security Solution sub teams -/x-pack/plugins/cases @elastic/security-threat-hunting-cases +/x-pack/plugins/cases @elastic/security-threat-hunting /x-pack/plugins/timelines @elastic/security-threat-hunting -/x-pack/test/case_api_integration @elastic/security-threat-hunting-cases +/x-pack/test/case_api_integration @elastic/security-threat-hunting /x-pack/plugins/lists @elastic/security-detections-response ## Security Solution sub teams - security-onboarding-and-lifecycle-mgt diff --git a/docs/apm/dependencies.asciidoc b/docs/apm/dependencies.asciidoc new file mode 100644 index 0000000000000..b3afdc4880df5 --- /dev/null +++ b/docs/apm/dependencies.asciidoc @@ -0,0 +1,32 @@ +[role="xpack"] +[[dependencies]] +=== Dependencies + +APM agents collect details about external calls made from instrumented services. +Sometimes, these external calls resolve into a downstream service that's instrumented -- in these cases, +you can utilize <> to drill down into problematic downstream services. +Other times, though, it's not possible to instrument a downstream dependency -- +like with a database or third-party service. +**Dependencies** gives you a window into these uninstrumented, downstream dependencies. + +[role="screenshot"] +image::apm/images/dependencies.png[Dependencies view in the APM app in Kibana] + +Many application issues are caused by slow or unresponsive downstream dependencies. +And because a single, slow dependency can significantly impact the end-user experience, +it's important to be able to quickly identify these problems and determine the root cause. + +Select a dependency to see detailed latency, throughput, and failed transaction rate metrics. + +[role="screenshot"] +image::apm/images/dependencies-drilldown.png[Dependencies drilldown view in the APM app in Kibana] + +When viewing a dependency, consider your pattern of usage with that dependency. +If your usage pattern _hasn't_ increased or decreased, +but the experience has been negatively effected -- either with an increase in latency or errors, +there's likely a problem with the dependency that needs to be addressed. + +If your usage pattern _has_ changed, the dependency view can quickly show you whether +that pattern change exists in all upstream services, or just a subset of your services. +You might then start digging into traces coming from +impacted services to determine why that pattern change has occurred. diff --git a/docs/apm/errors.asciidoc b/docs/apm/errors.asciidoc index c468d7f0235b2..d8fc75bf50340 100644 --- a/docs/apm/errors.asciidoc +++ b/docs/apm/errors.asciidoc @@ -4,19 +4,21 @@ TIP: {apm-overview-ref-v}/errors.html[Errors] are groups of exceptions with a similar exception or log message. -The *Errors* overview provides a high-level view of the error message and culprit, -the number of occurrences, and the most recent occurrence. -Just like the transaction overview, you'll notice we group together like errors. -This makes it very easy to quickly see which errors are affecting your services, +The *Errors* overview provides a high-level view of the exceptions that APM agents catch, +or that users manually report with APM agent APIs. +Like errors are grouped together to make it easy to quickly see which errors are affecting your services, and to take actions to rectify them. +A service returning a 5xx code from a request handler, controller, etc., will not create +an exception that an APM agent can catch, and will therefore not show up in this view. + [role="screenshot"] -image::apm/images/apm-errors-overview.png[Example view of the errors overview in the APM app in Kibana] +image::apm/images/apm-errors-overview.png[APM Errors overview] Selecting an error group ID or error message brings you to the *Error group*. [role="screenshot"] -image::apm/images/apm-error-group.png[Example view of the error group page in the APM app in Kibana] +image::apm/images/apm-error-group.png[APM Error group] Here, you'll see the error message, culprit, and the number of occurrences over time. diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc index e448c0beb8b99..6b205d0274262 100644 --- a/docs/apm/getting-started.asciidoc +++ b/docs/apm/getting-started.asciidoc @@ -29,6 +29,7 @@ start with: * <> * <> +* <> * <> Notice something awry? Select a service or trace and dive deeper with: @@ -46,6 +47,8 @@ include::services.asciidoc[] include::traces.asciidoc[] +include::dependencies.asciidoc[] + include::service-maps.asciidoc[] include::service-overview.asciidoc[] diff --git a/docs/apm/images/all-instances.png b/docs/apm/images/all-instances.png index e77c8af2c46f6..70028b5a9b58b 100644 Binary files a/docs/apm/images/all-instances.png and b/docs/apm/images/all-instances.png differ diff --git a/docs/apm/images/apm-distributed-tracing.png b/docs/apm/images/apm-distributed-tracing.png index 0dbffa591d43a..4d1b8cde20e95 100644 Binary files a/docs/apm/images/apm-distributed-tracing.png and b/docs/apm/images/apm-distributed-tracing.png differ diff --git a/docs/apm/images/apm-geo-ui.png b/docs/apm/images/apm-geo-ui.png index 5bbe713c908a4..69c1390a27989 100644 Binary files a/docs/apm/images/apm-geo-ui.png and b/docs/apm/images/apm-geo-ui.png differ diff --git a/docs/apm/images/apm-logs-tab.png b/docs/apm/images/apm-logs-tab.png index 891d2b7a1dd69..c79be8b5eb0b7 100644 Binary files a/docs/apm/images/apm-logs-tab.png and b/docs/apm/images/apm-logs-tab.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 7aeb5f1ac379f..271c0347aa53e 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-span-detail.png b/docs/apm/images/apm-span-detail.png index c9f55575b2232..d0b6a4de3d3df 100644 Binary files a/docs/apm/images/apm-span-detail.png and b/docs/apm/images/apm-span-detail.png differ diff --git a/docs/apm/images/apm-transaction-duration-dist.png b/docs/apm/images/apm-transaction-duration-dist.png index 91ae6c3a59ad2..9c7ab5dd67dc0 100644 Binary files a/docs/apm/images/apm-transaction-duration-dist.png and b/docs/apm/images/apm-transaction-duration-dist.png differ diff --git a/docs/apm/images/apm-transaction-sample.png b/docs/apm/images/apm-transaction-sample.png index 54eea902f0311..a9490fc20d853 100644 Binary files a/docs/apm/images/apm-transaction-sample.png and b/docs/apm/images/apm-transaction-sample.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index 66cf739a861b7..34cd0219b895d 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/apm-transactions-table.png b/docs/apm/images/apm-transactions-table.png index b573adfb0c450..8a3415bc9a9f1 100644 Binary files a/docs/apm/images/apm-transactions-table.png and b/docs/apm/images/apm-transactions-table.png differ diff --git a/docs/apm/images/dependencies-drilldown.png b/docs/apm/images/dependencies-drilldown.png new file mode 100644 index 0000000000000..4c491c1ffa254 Binary files /dev/null and b/docs/apm/images/dependencies-drilldown.png differ diff --git a/docs/apm/images/dependencies.png b/docs/apm/images/dependencies.png new file mode 100644 index 0000000000000..260025d31654b Binary files /dev/null and b/docs/apm/images/dependencies.png differ diff --git a/docs/apm/images/error-rate.png b/docs/apm/images/error-rate.png index 036c7a08302bd..845fa2af07de1 100644 Binary files a/docs/apm/images/error-rate.png and b/docs/apm/images/error-rate.png differ diff --git a/docs/apm/images/spans-dependencies.png b/docs/apm/images/spans-dependencies.png index d6e26a5061a6e..558099dd450c1 100644 Binary files a/docs/apm/images/spans-dependencies.png and b/docs/apm/images/spans-dependencies.png differ diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index f4d35a2d554ba..385d9921ae2b0 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -4,8 +4,7 @@ [partintro] -- -The APM app in {kib} is provided with the basic license. -It allows you to monitor your software services and applications in real-time; +The APM app in {kib} allows you to monitor your software services and applications in real-time; visualize detailed performance information on your services, identify and analyze errors, and monitor host-level and agent-specific metrics like JVM and Go runtime metrics. diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index f1096a4e43bbc..05537cef58c98 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -69,34 +69,43 @@ image::apm/images/traffic-transactions.png[Traffic and transactions] [discrete] [[service-error-rates]] -=== Error rate and errors +=== Failed transaction rate and errors -The *Error rate* chart displays the average error rates relating to the service, within a specific time range. -An HTTP response code greater than 400 does not necessarily indicate a failed transaction. -<>. +The failed transaction rate represents the percentage of failed transactions from the perspective of the selected service. +It's useful for visualizing unexpected increases, decreases, or irregular patterns in a service's transactions. ++ +[TIP] +==== +HTTP **transactions** from the HTTP server perspective do not consider a `4xx` status code (client error) as a failure +because the failure was caused by the caller, not the HTTP server. Thus, `event.outcome=success` and there will be no increase in failed transaction rate. + +HTTP **spans** from the client perspective however, are considered failures if the HTTP status code is ≥ 400. +These spans will set `event.outcome=failure` and increase the failed transaction rate. + +If there is no HTTP status, both transactions and spans are considered successful unless an error is reported. +==== The *Errors* table provides a high-level view of each error message when it first and last occurred, along with the total number of occurrences. This makes it very easy to quickly see which errors affect your services and take actions to rectify them. To do so, click *View errors*. [role="screenshot"] -image::apm/images/error-rate.png[Error rate and errors] +image::apm/images/error-rate.png[failed transaction rate and errors] [discrete] [[service-span-duration]] === Span types average duration and dependencies -The *Average duration by span type* chart visualizes each span type's average duration and helps you determine +The *Time spent by span type* chart visualizes each span type's average duration and helps you determine which spans could be slowing down transactions. The "app" label displayed under the chart indicates that something was happening within the application. This could signal that the agent does not have auto-instrumentation for whatever was happening during that time or that the time was spent in the application code and not in database or external requests. The *Dependencies* table displays a list of downstream services or external connections relevant -to the service at the selected time range. The table displays latency, traffic, error rate, and the impact of +to the service at the selected time range. The table displays latency, throughput, failed transaction rate, and the impact of each dependency. By default, dependencies are sorted by _Impact_ to show the most used and the slowest dependency. -If there is a particular dependency you are interested in, click *View service map* to view the related -<>. +If there is a particular dependency you are interested in, click *<>* to learn more about it. NOTE: Displaying dependencies for services instrumented with the Real User Monitoring (RUM) agent requires an agent version ≥ v5.6.3. @@ -106,11 +115,11 @@ image::apm/images/spans-dependencies.png[Span type duration and dependencies] [discrete] [[service-instances]] -=== All instances +=== Instances -The *All instances* table displays a list of all the available service instances within the selected time range. -Depending on how the service runs, the instance could be a host or a container. The table displays latency, traffic, -errors, CPU usage, and memory usage for each instance. By default, instances are sorted by _Throughput_. +The *Instances* table displays a list of all the available service instances within the selected time range. +Depending on how the service runs, the instance could be a host or a container. The table displays latency, throughput, +failed transaction, CPU usage, and memory usage for each instance. By default, instances are sorted by _Throughput_. [role="screenshot"] image::apm/images/all-instances.png[All instances] diff --git a/docs/apm/set-up.asciidoc b/docs/apm/set-up.asciidoc index b2e78bd08bc93..3cbe45ec913b7 100644 --- a/docs/apm/set-up.asciidoc +++ b/docs/apm/set-up.asciidoc @@ -8,7 +8,7 @@ APM is available via the navigation sidebar in {Kib}. If you have not already installed and configured Elastic APM, -the *Setup Instructions* in Kibana will get you started. +the *Add data* page will get you started. [role="screenshot"] image::apm/images/apm-setup.png[Installation instructions on the APM page in Kibana] @@ -17,10 +17,9 @@ image::apm/images/apm-setup.png[Installation instructions on the APM page in Kib [[apm-configure-index-pattern]] === Load the index pattern -Index patterns tell Kibana which Elasticsearch indices you want to explore. +Index patterns tell {kib} which {es} indices you want to explore. An APM index pattern is necessary for certain features in the APM app, like the query bar. -To set up the correct index pattern, -simply click *Load Kibana objects* at the bottom of the Setup Instructions. +To set up the correct index pattern, on the *Add data* page, click *Load Kibana objects*. [role="screenshot"] image::apm/images/apm-index-pattern.png[Setup index pattern for APM in Kibana] diff --git a/docs/apm/transactions.asciidoc b/docs/apm/transactions.asciidoc index 76006d375d075..c0850e4e9d507 100644 --- a/docs/apm/transactions.asciidoc +++ b/docs/apm/transactions.asciidoc @@ -8,7 +8,7 @@ APM agents automatically collect performance metrics on HTTP requests, database [role="screenshot"] image::apm/images/apm-transactions-overview.png[Example view of transactions table in the APM app in Kibana] -The *Latency*, *transactions per minute*, *Error rate*, and *Average duration by span type* +The *Latency*, *transactions per minute*, *Failed transaction rate*, and *Average duration by span type* charts display information on all transactions associated with the selected service: *Latency*:: @@ -23,17 +23,17 @@ Useful for determining if more responses than usual are being served with a part Like in the latency graph, you can zoom in on anomalies to further investigate them. [[transaction-error-rate]] -*Error rate*:: -The error rate represents the percentage of failed transactions from the perspective of the selected service. +*Failed transaction rate*:: +The failed transaction rate represents the percentage of failed transactions from the perspective of the selected service. It's useful for visualizing unexpected increases, decreases, or irregular patterns in a service's transactions. + [TIP] ==== HTTP **transactions** from the HTTP server perspective do not consider a `4xx` status code (client error) as a failure -because the failure was caused by the caller, not the HTTP server. Thus, there will be no increase in error rate. +because the failure was caused by the caller, not the HTTP server. Thus, `event.outcome=success` and there will be no increase in failed transaction rate. HTTP **spans** from the client perspective however, are considered failures if the HTTP status code is ≥ 400. -These spans will increase the error rate. +These spans will set `event.outcome=failure` and increase the failed transaction rate. If there is no HTTP status, both transactions and spans are considered successful unless an error is reported. ==== @@ -97,7 +97,7 @@ This page is visually similar to the transaction overview, but it shows data fro the selected transaction group. [role="screenshot"] -image::apm/images/apm-transaction-response-dist.png[Example view of response time distribution] +image::apm/images/apm-transactions-overview.png[Example view of response time distribution] [[transaction-duration-distribution]] ==== Latency distribution @@ -110,10 +110,10 @@ It's the requests on the right, the ones taking longer than average, that we pro [role="screenshot"] image::apm/images/apm-transaction-duration-dist.png[Example view of latency distribution graph] -Select a latency duration _bucket_ to display up to ten trace samples. +Click and drag to select a latency duration _bucket_ to display up to 500 trace samples. [[transaction-trace-sample]] -==== Trace sample +==== Trace samples Trace samples are based on the _bucket_ selection in the *Latency distribution* chart; update the samples by selecting a new _bucket_. @@ -167,4 +167,11 @@ and solve problems. [role="screenshot"] image::apm/images/apm-logs-tab.png[APM logs tab] -// To do: link to log correlation +[[transaction-latency-correlations]] +==== Correlations + +Correlations surface attributes of your data that are potentially correlated with high-latency or erroneous transactions. +To learn more, see <>. + +[role="screenshot"] +image::apm/images/correlations-hover.png[APM lattency correlations] diff --git a/package.json b/package.json index 588dc984651b7..97768601ff852 100644 --- a/package.json +++ b/package.json @@ -110,14 +110,14 @@ "@elastic/search-ui-app-search-connector": "^1.6.0", "@emotion/react": "^11.4.0", "@hapi/accept": "^5.0.2", - "@hapi/boom": "^9.1.1", + "@hapi/boom": "^9.1.4", "@hapi/cookie": "^11.0.2", "@hapi/good-squeeze": "6.0.0", "@hapi/h2o2": "^9.1.0", - "@hapi/hapi": "^20.0.3", - "@hapi/hoek": "^9.1.1", - "@hapi/inert": "^6.0.3", - "@hapi/podium": "^4.1.1", + "@hapi/hapi": "^20.2.0", + "@hapi/hoek": "^9.2.0", + "@hapi/inert": "^6.0.4", + "@hapi/podium": "^4.1.3", "@hapi/wreck": "^17.1.0", "@kbn/ace": "link:bazel-bin/packages/kbn-ace", "@kbn/alerts": "link:bazel-bin/packages/kbn-alerts", @@ -528,11 +528,10 @@ "@types/glob": "^7.1.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", - "@types/hapi__cookie": "^10.1.1", - "@types/hapi__h2o2": "^8.3.2", - "@types/hapi__hapi": "^20.0.2", - "@types/hapi__inert": "^5.2.2", - "@types/hapi__podium": "^3.4.1", + "@types/hapi__cookie": "^10.1.3", + "@types/hapi__h2o2": "^8.3.3", + "@types/hapi__hapi": "^20.0.9", + "@types/hapi__inert": "^5.2.3", "@types/has-ansi": "^3.0.0", "@types/he": "^1.1.1", "@types/history": "^4.7.3", diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts index 6d12d5d05f07c..b198e6139d5d7 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts @@ -131,7 +131,7 @@ export class BasePathProxyServer { agent: this.httpsAgent, passThrough: true, xforward: true, - mapUri: async (request) => { + mapUri: async (request: Request) => { return { // Passing in this header to merge it is a workaround until this is fixed: // https://github.com/hapijs/h2o2/issues/124 diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel index 1148cf1d38b65..c4927fe076e15 100644 --- a/packages/kbn-legacy-logging/BUILD.bazel +++ b/packages/kbn-legacy-logging/BUILD.bazel @@ -29,6 +29,7 @@ RUNTIME_DEPS = [ "//packages/kbn-utils", "@npm//@elastic/numeral", "@npm//@hapi/hapi", + "@npm//@hapi/podium", "@npm//chokidar", "@npm//lodash", "@npm//moment-timezone", @@ -41,12 +42,12 @@ TYPES_DEPS = [ "//packages/kbn-config-schema", "//packages/kbn-utils", "@npm//@elastic/numeral", + "@npm//@hapi/podium", "@npm//chokidar", "@npm//query-string", "@npm//rxjs", "@npm//tslib", "@npm//@types/hapi__hapi", - "@npm//@types/hapi__podium", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/moment-timezone", diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index c02eb2803515a..f6c42dd1b161f 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -112,7 +112,6 @@ export class LegacyLoggingServer { tags: [getLegacyLogLevel(level), ...context.split('.'), ...tags], timestamp: timestamp.getTime(), }) - // @ts-expect-error @hapi/podium emit is actually an async function .catch((err) => { // eslint-disable-next-line no-console console.error('An unexpected error occurred while writing to the log:', err.stack); diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 61ba8eb157ee3..9837d45ddd869 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -33,6 +33,28 @@ describe('createRouter', () => { }, }, children: [ + { + path: '/services/{serviceName}/errors', + element: <>, + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + }), + children: [ + { + path: '/services/{serviceName}/errors/{groupId}', + element: <>, + params: t.type({ + path: t.type({ groupId: t.string }), + }), + }, + { + path: '/services/{serviceName}/errors', + element: <>, + }, + ], + }, { path: '/services', element: <>, @@ -43,7 +65,7 @@ describe('createRouter', () => { }), }, { - path: '/services', + path: '/services/{serviceName}', element: <>, children: [ { @@ -252,6 +274,28 @@ describe('createRouter', () => { }, }); }); + + it('matches deep routes', () => { + history.push('/services/opbeans-java/errors/foo?rangeFrom=now-15m&rangeTo=now'); + + const matchedRoutes = router.matchRoutes( + '/services/{serviceName}/errors/{groupId}', + history.location + ); + + expect(matchedRoutes.length).toEqual(4); + + expect(matchedRoutes[matchedRoutes.length - 1].match).toEqual({ + isExact: true, + params: { + path: { + groupId: 'foo', + }, + }, + path: '/services/:serviceName/errors/:groupId', + url: '/services/opbeans-java/errors/foo', + }); + }); }); describe('link', () => { diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 7f2ac818fc9b9..13f09e7546de5 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -26,7 +26,7 @@ const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped; const mergeRt: typeof mergeRtTyped = mergeRtNonTyped; function toReactRouterPath(path: string) { - return path.replace(/(?:{([^\/]+)})/, ':$1'); + return path.replace(/(?:{([^\/]+)})/g, ':$1'); } export function createRouter(routes: TRoutes): Router { diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index de9e4d4496f3b..f348936d26795 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -804,6 +804,21 @@ describe('#start()', () => { `); }); + it("when openInNewTab is true it doesn't update currentApp$ after mounting", async () => { + service.setup(setupDeps); + + const { currentAppId$, navigateToApp } = await service.start(startDeps); + const stop$ = new Subject(); + const promise = currentAppId$.pipe(bufferCount(4), takeUntil(stop$)).toPromise(); + + await navigateToApp('delta', { openInNewTab: true }); + stop$.next(); + + const appIds = await promise; + + expect(appIds).toBeUndefined(); + }); + it('updates httpLoadingCount$ while mounting', async () => { // Use a memory history so that mounting the component will work const { createMemoryHistory } = jest.requireActual('history'); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 2e804bf2f5413..3ba0d78cf15fd 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -250,16 +250,15 @@ export class ApplicationService { if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } - if (!navigatingToSameApp) { - this.appInternalStates.delete(this.currentAppId$.value!); - } if (openInNewTab) { this.openInNewTab!(getAppUrl(availableMounters, appId, path)); } else { + if (!navigatingToSameApp) { + this.appInternalStates.delete(this.currentAppId$.value!); + } this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); + this.currentAppId$.next(appId); } - - this.currentAppId$.next(appId); } }; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index e6f7974beac2f..0fe1347d299f9 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -426,6 +426,7 @@ export class DocLinksService { fail: `${ELASTICSEARCH_DOCS}fail-processor.html`, foreach: `${ELASTICSEARCH_DOCS}foreach-processor.html`, geoIp: `${ELASTICSEARCH_DOCS}geoip-processor.html`, + geoMatch: `${ELASTICSEARCH_DOCS}geo-match-enrich-policy-type.html`, grok: `${ELASTICSEARCH_DOCS}grok-processor.html`, gsub: `${ELASTICSEARCH_DOCS}gsub-processor.html`, htmlString: `${ELASTICSEARCH_DOCS}htmlstrip-processor.html`, diff --git a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts index 936746ddc6930..32d12e13434aa 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/integration_tests/actions.test.ts @@ -802,7 +802,9 @@ describe('migration actions', () => { } `); }); - it('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { + + // FLAKY https://github.com/elastic/kibana/issues/113012 + it.skip('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { const res = (await reindex({ client, sourceIndex: 'existing_index_with_docs', diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index c5a4ff64d2188..21f223a09f60d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -47,7 +47,7 @@ export async function runDockerGenerator( // General docker var config const license = 'Elastic License'; - const imageTag = 'docker.elastic.co/kibana/kibana'; + const imageTag = `docker.elastic.co/kibana${flags.cloud ? '-ci' : ''}/kibana`; const version = config.getBuildVersion(); const artifactArchitecture = flags.architecture === 'aarch64' ? 'aarch64' : 'x86_64'; const artifactPrefix = `kibana-${version}-linux`; diff --git a/src/plugins/data/common/data_views/data_views/data_view.ts b/src/plugins/data/common/data_views/data_views/data_view.ts index c61f5f7f31e3a..18d301d2f9ea7 100644 --- a/src/plugins/data/common/data_views/data_views/data_view.ts +++ b/src/plugins/data/common/data_views/data_views/data_view.ts @@ -10,6 +10,7 @@ import _, { each, reject } from 'lodash'; import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import type { estypes } from '@elastic/elasticsearch'; import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '../..'; import type { RuntimeField } from '../types'; import { DuplicateField } from '../../../../kibana_utils/common'; @@ -158,7 +159,7 @@ export class DataView implements IIndexPattern { }; getComputedFields() { - const scriptFields: Record = {}; + const scriptFields: Record = {}; if (!this.fields) { return { storedFields: ['*'], @@ -185,7 +186,7 @@ export class DataView implements IIndexPattern { scriptFields[field.name] = { script: { source: field.script as string, - lang: field.lang as string, + lang: field.lang, }, }; }); diff --git a/src/plugins/data/common/data_views/types.ts b/src/plugins/data/common/data_views/types.ts index d1e822aea4e97..85fe98fbcfeb7 100644 --- a/src/plugins/data/common/data_views/types.ts +++ b/src/plugins/data/common/data_views/types.ts @@ -89,8 +89,8 @@ export type OnNotification = (toastInputFields: ToastInputFields) => void; export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; export interface UiSettingsCommon { - get: (key: string) => Promise; - getAll: () => Promise>; + get: (key: string) => Promise; + getAll: () => Promise>; set: (key: string, value: any) => Promise; remove: (key: string) => Promise; } diff --git a/src/plugins/data/public/data_views/ui_settings_wrapper.ts b/src/plugins/data/public/data_views/ui_settings_wrapper.ts index e0998ed72b2e6..f8ae317391fa3 100644 --- a/src/plugins/data/public/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data/public/data_views/ui_settings_wrapper.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient, PublicUiSettingsParams, UserProvidedValues } from 'src/core/public'; import { UiSettingsCommon } from '../../common'; export class UiSettingsPublicToCommon implements UiSettingsCommon { @@ -14,11 +14,11 @@ export class UiSettingsPublicToCommon implements UiSettingsCommon { constructor(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; } - get(key: string) { + get(key: string): Promise { return Promise.resolve(this.uiSettings.get(key)); } - getAll() { + getAll(): Promise>> { return Promise.resolve(this.uiSettings.getAll()); } diff --git a/src/plugins/data/server/data_views/routes.ts b/src/plugins/data/server/data_views/routes.ts index 32fa50940bca7..9488285fc7e2c 100644 --- a/src/plugins/data/server/data_views/routes.ts +++ b/src/plugins/data/server/data_views/routes.ts @@ -7,7 +7,7 @@ */ import { schema } from '@kbn/config-schema'; -import { HttpServiceSetup, RequestHandlerContext, StartServicesAccessor } from 'kibana/server'; +import { HttpServiceSetup, StartServicesAccessor } from 'kibana/server'; import { IndexPatternsFetcher } from './fetcher'; import { registerCreateIndexPatternRoute } from './routes/create_index_pattern'; import { registerGetIndexPatternRoute } from './routes/get_index_pattern'; @@ -154,7 +154,7 @@ export function registerRoutes( }), }, }, - async (context: RequestHandlerContext, request: any, response: any) => { + async (context, request, response) => { const { asCurrentUser } = context.core.elasticsearch.client; const indexPatterns = new IndexPatternsFetcher(asCurrentUser); const { pattern, interval, look_back: lookBack, meta_fields: metaFields } = request.query; diff --git a/src/plugins/data/server/data_views/ui_settings_wrapper.ts b/src/plugins/data/server/data_views/ui_settings_wrapper.ts index 3b00aab7d6bdd..dce552205db2e 100644 --- a/src/plugins/data/server/data_views/ui_settings_wrapper.ts +++ b/src/plugins/data/server/data_views/ui_settings_wrapper.ts @@ -14,11 +14,11 @@ export class UiSettingsServerToCommon implements UiSettingsCommon { constructor(uiSettings: IUiSettingsClient) { this.uiSettings = uiSettings; } - get(key: string) { + get(key: string): Promise { return this.uiSettings.get(key); } - getAll() { + getAll(): Promise> { return this.uiSettings.getAll(); } diff --git a/src/plugins/discover/public/application/services/use_es_doc_search.ts b/src/plugins/discover/public/application/services/use_es_doc_search.ts index c5216c483fd10..a2f0cd6f8442b 100644 --- a/src/plugins/discover/public/application/services/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/services/use_es_doc_search.ts @@ -37,7 +37,7 @@ export function buildSearchBody( }, }, stored_fields: computedFields.storedFields, - script_fields: computedFields.scriptFields as Record, + script_fields: computedFields.scriptFields, version: true, }, }; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index b0e0b8b2298ab..0ac4c61f4a711 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { PluginInitializerContext } from 'src/core/public'; +import { KibanaUtilsPublicPlugin } from './plugin'; + // TODO: https://github.com/elastic/kibana/issues/109893 /* eslint-disable @kbn/eslint/no_export_all */ @@ -78,10 +81,8 @@ export { export { applyDiff } from './state_management/utils/diff_object'; export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter'; -/** dummy plugin, we just want kibanaUtils to have its own bundle */ -export function plugin() { - return new (class KibanaUtilsPlugin { - setup() {} - start() {} - })(); +export { KibanaUtilsSetup } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new KibanaUtilsPublicPlugin(initializerContext); } diff --git a/src/plugins/kibana_utils/public/mocks.ts b/src/plugins/kibana_utils/public/mocks.ts new file mode 100644 index 0000000000000..a537c2fc74e90 --- /dev/null +++ b/src/plugins/kibana_utils/public/mocks.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaUtilsSetup, KibanaUtilsStart } from './plugin'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + return { + setVersion: jest.fn(), + }; +}; + +const createStartContract = (): Start => { + return undefined; +}; + +export const kibanaUtilsPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/kibana_utils/public/plugin.ts b/src/plugins/kibana_utils/public/plugin.ts new file mode 100644 index 0000000000000..b255aa34ccfdb --- /dev/null +++ b/src/plugins/kibana_utils/public/plugin.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { History } from 'history'; +import { setVersion } from './set_version'; + +export interface KibanaUtilsSetup { + setVersion: (history: Pick) => void; +} + +export type KibanaUtilsStart = undefined; + +export class KibanaUtilsPublicPlugin implements Plugin { + private readonly version: string; + + constructor(initializerContext: PluginInitializerContext) { + this.version = initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup): KibanaUtilsSetup { + return { + setVersion: this.setVersion, + }; + } + + public start(core: CoreStart): KibanaUtilsStart { + return undefined; + } + + public stop() {} + + private setVersion = (history: Pick) => { + setVersion(history, this.version); + }; +} diff --git a/src/plugins/kibana_utils/public/set_version.test.ts b/src/plugins/kibana_utils/public/set_version.test.ts new file mode 100644 index 0000000000000..eb70d889d0f03 --- /dev/null +++ b/src/plugins/kibana_utils/public/set_version.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { History } from 'history'; +import { setVersion } from './set_version'; + +describe('setVersion', () => { + test('sets version, if one is not set', () => { + const history: Pick = { + location: { + hash: '', + search: '', + pathname: '/', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '1.2.3'); + + expect(history.replace).toHaveBeenCalledTimes(1); + expect(history.replace).toHaveBeenCalledWith('/?_v=1.2.3'); + }); + + test('overwrites, if version already set to a different value', () => { + const history: Pick = { + location: { + hash: '/view/dashboards', + search: 'a=b&_v=7.16.6', + pathname: '/foo/bar', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '8.0.0'); + + expect(history.replace).toHaveBeenCalledTimes(1); + expect(history.replace).toHaveBeenCalledWith('/foo/bar?a=b&_v=8.0.0#/view/dashboards'); + }); + + test('does nothing, if version already set to correct value', () => { + const history: Pick = { + location: { + hash: '/view/dashboards', + search: 'a=b&_v=8.0.0', + pathname: '/foo/bar', + state: {}, + }, + replace: jest.fn(), + }; + setVersion(history, '8.0.0'); + + expect(history.replace).toHaveBeenCalledTimes(0); + }); +}); diff --git a/src/plugins/kibana_utils/public/set_version.ts b/src/plugins/kibana_utils/public/set_version.ts new file mode 100644 index 0000000000000..b3acb39ed5134 --- /dev/null +++ b/src/plugins/kibana_utils/public/set_version.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { History } from 'history'; + +export const setVersion = (history: Pick, version: string) => { + const search = new URLSearchParams(history.location.search); + if (search.get('_v') === version) return; + search.set('_v', version); + const path = + history.location.pathname + + '?' + + search.toString() + + (history.location.hash ? '#' + history.location.hash : ''); + history.replace(path); +}; diff --git a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts index 3399d0628ad65..9772e693358b6 100644 --- a/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts +++ b/src/plugins/vis_types/vega/public/vega_view/vega_map_view/map_service_settings.ts @@ -66,6 +66,11 @@ export class MapServiceSettings { tileApiUrl: this.config.emsTileApiUrl, landingPageUrl: this.config.emsLandingPageUrl, }); + + // Allow zooms > 10 for Vega Maps + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this.emsClient.addQueryParams({ license: 'sspl' }); } public async getTmsService(tmsTileLayer: string) { diff --git a/test/functional/apps/dashboard/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/dashboard_unsaved_state.ts index 8043c8bf8cc37..c2da82a96cd0c 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/dashboard_unsaved_state.ts @@ -24,7 +24,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let unsavedPanelCount = 0; const testQuery = 'Test Query'; - describe('dashboard unsaved state', () => { + // FLAKY https://github.com/elastic/kibana/issues/112812 + describe.skip('dashboard unsaved state', () => { before(async () => { await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts index 0bc32672d41b9..244d07d2cfc82 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts @@ -37,7 +37,8 @@ export default function ({ }: FtrProviderContext & { updateBaselines: boolean }) { let expectExpression: ExpectExpression; - describe('esaggs timeshift tests', () => { + // FLAKY https://github.com/elastic/kibana/issues/107028 + describe.skip('esaggs timeshift tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh index 00cc0d78599dd..17ca46b0097b1 100755 --- a/test/scripts/jenkins_storybook.sh +++ b/test/scripts/jenkins_storybook.sh @@ -20,6 +20,7 @@ yarn storybook --site expression_repeat_image yarn storybook --site expression_reveal_image yarn storybook --site expression_shape yarn storybook --site expression_tagcloud +yarn storybook --site fleet yarn storybook --site infra yarn storybook --site security_solution yarn storybook --site ui_actions_enhanced diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index cff876b5995a1..1e51adf3e9d09 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -15,3 +15,10 @@ export * from './rewrite_request_case'; export const BASE_ACTION_API_PATH = '/api/actions'; export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions'; export const ACTIONS_FEATURE_ID = 'actions'; + +// supported values for `service` in addition to nodemailer's list of well-known services +export enum AdditionalEmailServices { + ELASTIC_CLOUD = 'elastic_cloud', + EXCHANGE = 'exchange_server', + OTHER = 'other', +} diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index a341cdf58b9e2..7549d2ecaab77 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -20,6 +20,7 @@ import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '../../licensing/server/mocks'; import { httpServerMock } from '../../../../src/core/server/mocks'; import { auditServiceMock } from '../../security/server/audit/index.mock'; +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { elasticsearchServiceMock, @@ -28,7 +29,12 @@ import { import { actionExecutorMock } from './lib/action_executor.mock'; import uuid from 'uuid'; import { ActionsAuthorization } from './authorization/actions_authorization'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../src/core/server/elasticsearch/client/mocks'; @@ -38,6 +44,22 @@ jest.mock('../../../../src/core/server/saved_objects/service/lib/utils', () => ( }, })); +jest.mock('./lib/track_legacy_rbac_exemption', () => ({ + trackLegacyRBACExemption: jest.fn(), +})); + +jest.mock('./authorization/get_authorization_mode_by_source', () => { + return { + getAuthorizationModeBySource: jest.fn(() => { + return 1; + }), + AuthorizationMode: { + Legacy: 0, + RBAC: 1, + }, + }; +}); + const defaultKibanaIndex = '.kibana'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -47,6 +69,8 @@ const executionEnqueuer = jest.fn(); const ephemeralExecutionEnqueuer = jest.fn(); const request = httpServerMock.createKibanaRequest(); const auditLogger = auditServiceMock.create().asScoped(request); +const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); +const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const mockTaskManager = taskManagerMock.createSetup(); @@ -82,6 +106,7 @@ beforeEach(() => { request, authorization: authorization as unknown as ActionsAuthorization, auditLogger, + usageCounter: mockUsageCounter, }); }); @@ -1640,6 +1665,9 @@ describe('update()', () => { describe('execute()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.execute({ actionId: 'action-id', params: { @@ -1650,6 +1678,9 @@ describe('execute()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1665,6 +1696,21 @@ describe('execute()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.execute({ + actionId: 'action-id', + params: { + name: 'my name', + }, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith('execute', mockUsageCounter); + }); }); test('calls the actionExecutor with the appropriate parameters', async () => { @@ -1756,6 +1802,9 @@ describe('execute()', () => { describe('enqueueExecution()', () => { describe('authorization', () => { test('ensures user is authorised to excecute actions', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); await actionsClient.enqueueExecution({ id: uuid.v4(), params: {}, @@ -1766,6 +1815,9 @@ describe('enqueueExecution()', () => { }); test('throws when user is not authorised to create the type of action', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.RBAC; + }); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to execute all actions`) ); @@ -1781,6 +1833,24 @@ describe('enqueueExecution()', () => { expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); + + test('tracks legacy RBAC', async () => { + (getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => { + return AuthorizationMode.Legacy; + }); + + await actionsClient.enqueueExecution({ + id: uuid.v4(), + params: {}, + spaceId: 'default', + apiKey: null, + }); + + expect(trackLegacyRBACExemption as jest.Mock).toBeCalledWith( + 'enqueueExecution', + mockUsageCounter + ); + }); }); test('calls the executionEnqueuer with the appropriate parameters', async () => { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index d6f6037ecd8b8..b391e50283ad1 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import type { estypes } from '@elastic/elasticsearch'; +import { UsageCounter } from 'src/plugins/usage_collection/server'; import { i18n } from '@kbn/i18n'; import { omitBy, isUndefined } from 'lodash'; @@ -42,6 +43,7 @@ import { } from './authorization/get_authorization_mode_by_source'; import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events'; import { RunNowResult } from '../../task_manager/server'; +import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -74,6 +76,7 @@ interface ConstructorOptions { request: KibanaRequest; authorization: ActionsAuthorization; auditLogger?: AuditLogger; + usageCounter?: UsageCounter; } export interface UpdateOptions { @@ -93,6 +96,7 @@ export class ActionsClient { private readonly executionEnqueuer: ExecutionEnqueuer; private readonly ephemeralExecutionEnqueuer: ExecutionEnqueuer; private readonly auditLogger?: AuditLogger; + private readonly usageCounter?: UsageCounter; constructor({ actionTypeRegistry, @@ -106,6 +110,7 @@ export class ActionsClient { request, authorization, auditLogger, + usageCounter, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; @@ -118,6 +123,7 @@ export class ActionsClient { this.request = request; this.authorization = authorization; this.auditLogger = auditLogger; + this.usageCounter = usageCounter; } /** @@ -478,6 +484,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('execute', this.usageCounter); } return this.actionExecutor.execute({ actionId, @@ -495,6 +503,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('enqueueExecution', this.usageCounter); } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); } @@ -506,6 +516,8 @@ export class ActionsClient { AuthorizationMode.RBAC ) { await this.authorization.ensureAuthorized('execute'); + } else { + trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.usageCounter); } return this.ephemeralExecutionEnqueuer(this.unsecuredSavedObjectsClient, options); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index d10046341b268..fcd003286d5bb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -17,6 +17,7 @@ import { Logger } from '../../../../../src/core/server'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; import { renderMustacheString, renderMustacheObject } from '../lib/mustache_renderer'; +import { AdditionalEmailServices } from '../../common'; export type EmailActionType = ActionType< ActionTypeConfigType, @@ -33,13 +34,6 @@ export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions< // config definition export type ActionTypeConfigType = TypeOf; -// supported values for `service` in addition to nodemailer's list of well-known services -export enum AdditionalEmailServices { - ELASTIC_CLOUD = 'elastic_cloud', - EXCHANGE = 'exchange_server', - OTHER = 'other', -} - // these values for `service` require users to fill in host/port/secure export const CUSTOM_HOST_PORT_SERVICES: string[] = [AdditionalEmailServices.OTHER]; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts index 09080ee0c0063..b632cdf5f5219 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/request_oauth_client_credentials_token.ts @@ -7,6 +7,7 @@ import qs from 'query-string'; import axios from 'axios'; +import stringify from 'json-stable-stringify'; import { Logger } from '../../../../../../src/core/server'; import { request } from './axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; @@ -59,7 +60,7 @@ export async function requestOAuthClientCredentialsToken( expiresIn: res.data.expires_in, }; } else { - const errString = JSON.stringify(res.data); + const errString = stringify(res.data); logger.warn( `error thrown getting the access token from ${tokenUrl} for clientID: ${clientId}: ${errString}` ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index ea3c0f91b6a5c..53c70fddc5a09 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -13,10 +13,10 @@ import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; -import { AdditionalEmailServices } from '../email'; import { sendEmailGraphApi } from './send_email_graph_api'; import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; import { ProxySettings } from '../../types'; +import { AdditionalEmailServices } from '../../../common'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts index 10e9a3bc8d27c..ea1579095bb97 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email_graph_api.ts @@ -5,6 +5,8 @@ * 2.0. */ +// @ts-expect-error missing type def +import stringify from 'json-stringify-safe'; import axios, { AxiosResponse } from 'axios'; import { Logger } from '../../../../../../src/core/server'; import { request } from './axios_utils'; @@ -41,9 +43,9 @@ export async function sendEmailGraphApi( validateStatus: () => true, }); if (res.status === 202) { - return res; + return res.data; } - const errString = JSON.stringify(res.data); + const errString = stringify(res.data); logger.warn( `error thrown sending Microsoft Exchange email for clientID: ${sendEmailOptions.options.transport.clientId}: ${errString}` ); diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.ts new file mode 100644 index 0000000000000..ffd8e7f17c11f --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; +import { trackLegacyRBACExemption } from './track_legacy_rbac_exemption'; + +describe('trackLegacyRBACExemption', () => { + it('should call `usageCounter.incrementCounter`', () => { + const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); + const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); + + trackLegacyRBACExemption('test', mockUsageCounter); + expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `source_test`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + }); + + it('should do nothing if no usage counter is provided', () => { + let err; + try { + trackLegacyRBACExemption('test', undefined); + } catch (e) { + err = e; + } + expect(err).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts new file mode 100644 index 0000000000000..73c859c4cd21e --- /dev/null +++ b/x-pack/plugins/actions/server/lib/track_legacy_rbac_exemption.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UsageCounter } from 'src/plugins/usage_collection/server'; + +export function trackLegacyRBACExemption(source: string, usageCounter?: UsageCounter) { + if (usageCounter) { + usageCounter.incrementCounter({ + counterName: `source_${source}`, + counterType: 'legacyRBACExemption', + incrementBy: 1, + }); + } +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index fe133ddb6f0ac..78808b669d9e9 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { UsageCollectionSetup, UsageCounter } from 'src/plugins/usage_collection/server'; import { PluginInitializerContext, Plugin, @@ -151,6 +151,7 @@ export class ActionsPlugin implements Plugin(), this.licenseState, - usageCounter + this.usageCounter ); // Cleanup failed execution task definition @@ -367,6 +368,7 @@ export class ActionsPlugin implements Plugin; export type CasesClientPostRequest = rt.TypeOf; export type CasePostRequest = rt.TypeOf; export type CaseResponse = rt.TypeOf; +export type CaseResolveResponse = rt.TypeOf; export type CasesResponse = rt.TypeOf; export type CasesFindRequest = rt.TypeOf; export type CasesByAlertIDRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index c89c3eb08263b..948b203af14a8 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -114,6 +114,12 @@ export interface Case extends BasicCase { type: CaseType; } +export interface ResolvedCase { + case: Case; + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; + aliasTargetId?: string; +} + export interface QueryParams { page: number; perPage: number; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts index 18370be61bdf1..09f0215f5629f 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts @@ -13,6 +13,7 @@ import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_r import { StartServices } from '../../../types'; import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { securityMock } from '../../../../../security/public/mocks'; +import { spacesPluginMock } from '../../../../../spaces/public/mocks'; import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks'; export const createStartServicesMock = (): StartServices => @@ -25,6 +26,7 @@ export const createStartServicesMock = (): StartServices => }, security: securityMock.createStart(), triggersActionsUi: triggersActionsUiMock.createStart(), + spaces: spacesPluginMock.createStartContract(), } as unknown as StartServices); export const createWithKibanaMock = () => { diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index f12c8ba098d43..6fc9e1719e1cf 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -18,6 +18,7 @@ import { getAlertUserAction, } from '../../containers/mock'; import { TestProviders } from '../../common/mock'; +import { SpacesApi } from '../../../../spaces/public'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCase } from '../../containers/use_get_case'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; @@ -47,6 +48,13 @@ const useConnectorsMock = useConnectors as jest.Mock; const usePostPushToServiceMock = usePostPushToService as jest.Mock; const useKibanaMock = useKibana as jest.Mocked; +const spacesUiApiMock = { + redirectLegacyUrl: jest.fn().mockResolvedValue(undefined), + components: { + getLegacyUrlConflict: jest.fn().mockReturnValue(
), + }, +}; + const alertsHit = [ { _id: 'alert-id-1', @@ -138,6 +146,7 @@ describe('CaseView ', () => { isLoading: false, isError: false, data, + resolveOutcome: 'exactMatch', updateCase, fetchCase, }; @@ -174,6 +183,7 @@ describe('CaseView ', () => { actionTypeTitle: '.servicenow', iconClass: 'logoSecurity', }); + useKibanaMock().services.spaces = { ui: spacesUiApiMock } as unknown as SpacesApi; }); it('should render CaseComponent', async () => { @@ -395,36 +405,7 @@ describe('CaseView ', () => { })); const wrapper = mount( - + ); await waitFor(() => { @@ -439,36 +420,7 @@ describe('CaseView ', () => { })); const wrapper = mount( - + ); await waitFor(() => { @@ -477,43 +429,66 @@ describe('CaseView ', () => { }); it('should return case view when data is there', async () => { - (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'exactMatch', + })); const wrapper = mount( - + ); await waitFor(() => { expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled(); + expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); + + it('should redirect case view when resolves to alias match', async () => { + const resolveAliasId = `${defaultGetCase.data.id}_2`; + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'aliasMatch', + resolveAliasId, + })); + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled(); + expect(spacesUiApiMock.redirectLegacyUrl).toHaveBeenCalledWith( + `cases/${resolveAliasId}`, + 'case' + ); + }); + }); + + it('should redirect case view when resolves to conflict', async () => { + const resolveAliasId = `${defaultGetCase.data.id}_2`; + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + resolveOutcome: 'conflict', + resolveAliasId, + })); + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="conflict-component"]').exists()).toBeTruthy(); + expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled(); + expect(spacesUiApiMock.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + objectNoun: 'case', + currentObjectId: defaultGetCase.data.id, + otherObjectId: resolveAliasId, + otherObjectPath: `cases/${resolveAliasId}`, + }); }); }); @@ -521,41 +496,12 @@ describe('CaseView ', () => { (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); const wrapper = mount( - + ); wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click'); await waitFor(() => { - expect(fetchCaseUserActions).toBeCalledWith('1234', 'resilient-2', undefined); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id, 'resilient-2', undefined); expect(fetchCase).toBeCalled(); }); }); diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index bb0b894238b9d..81e7607c9011f 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -40,6 +40,7 @@ import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; import { getConnectorById } from '../utils'; import { DoesNotExist } from './does_not_exist'; +import { useKibana } from '../../common/lib/kibana'; export interface CaseViewComponentProps { allCasesNavigation: CasesNavigation; @@ -499,6 +500,14 @@ export const CaseComponent = React.memo( } ); +export const CaseViewLoading = () => ( + + + + + +); + export const CaseView = React.memo( ({ allCasesNavigation, @@ -518,27 +527,59 @@ export const CaseView = React.memo( refreshRef, hideSyncAlerts, }: CaseViewProps) => { - const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); - if (isError) { - return ; - } - if (isLoading) { - return ( - - - - - - ); - } - if (onCaseDataSuccess && data) { - onCaseDataSuccess(data); - } + const { data, resolveOutcome, resolveAliasId, isLoading, isError, fetchCase, updateCase } = + useGetCase(caseId, subCaseId); + const { spaces: spacesApi, http } = useKibana().services; - return ( + useEffect(() => { + if (onCaseDataSuccess && data) { + onCaseDataSuccess(data); + } + }, [data, onCaseDataSuccess]); + + useEffect(() => { + if (spacesApi && resolveOutcome === 'aliasMatch' && resolveAliasId != null) { + // CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and + // Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded + // under any another path, passing a path builder function by props from every parent plugin. + const newPath = http.basePath.prepend( + `cases/${resolveAliasId}${window.location.search}${window.location.hash}` + ); + spacesApi.ui.redirectLegacyUrl(newPath, i18n.CASE); + } + }, [resolveOutcome, resolveAliasId, spacesApi, http]); + + const getLegacyUrlConflictCallout = useCallback(() => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if (data && spacesApi && resolveOutcome === 'conflict' && resolveAliasId != null) { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const otherObjectId = resolveAliasId; // This is always defined if outcome === 'conflict' + // CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and + // Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded + // under any another path, passing a path builder function by props from every parent plugin. + const otherObjectPath = http.basePath.prepend( + `cases/${otherObjectId}${window.location.search}${window.location.hash}` + ); + return spacesApi.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.CASE, + currentObjectId: data.id, + otherObjectId, + otherObjectPath, + }); + } + return null; + }, [data, resolveAliasId, resolveOutcome, spacesApi, http.basePath]); + + return isError ? ( + + ) : isLoading ? ( + + ) : ( data && ( + {getLegacyUrlConflictCallout()} => Promise.resolve(basicCase); +export const resolveCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => Promise.resolve(basicResolvedCase); + export const getCasesStatus = async (signal: AbortSignal): Promise => Promise.resolve(casesStatus); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index e47930e81fe6b..654ade308ed44 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -30,6 +30,7 @@ import { postCase, postComment, pushCase, + resolveCase, } from './api'; import { @@ -68,7 +69,7 @@ describe('Case Configuration API', () => { }); const data = ['1', '2']; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await deleteCases(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'DELETE', @@ -77,7 +78,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await deleteCases(data, abortCtrl.signal); expect(resp).toEqual(''); }); @@ -89,7 +90,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(actionLicenses); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getActionLicense(abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/actions/connector_types`, { method: 'GET', @@ -97,7 +98,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getActionLicense(abortCtrl.signal); expect(resp).toEqual(actionLicenses); }); @@ -110,7 +111,7 @@ describe('Case Configuration API', () => { }); const data = basicCase.id; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCase(data, true, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, { method: 'GET', @@ -119,18 +120,46 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCase(data, true, abortCtrl.signal); expect(resp).toEqual(basicCase); }); }); + describe('resolveCase', () => { + const targetAliasId = '12345'; + const basicResolveCase = { + outcome: 'aliasMatch', + case: basicCaseSnake, + }; + const caseId = basicCase.id; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue({ ...basicResolveCase, target_alias_id: targetAliasId }); + }); + + test('should be called with correct check url, method, signal', async () => { + await resolveCase(caseId, true, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${caseId}/resolve`, { + method: 'GET', + query: { includeComments: true }, + signal: abortCtrl.signal, + }); + }); + + test('should return correct response', async () => { + const resp = await resolveCase(caseId, true, abortCtrl.signal); + expect(resp).toEqual({ ...basicResolveCase, case: basicCase, targetAliasId }); + }); + }); + describe('getCases', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(allCasesSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, @@ -148,7 +177,7 @@ describe('Case Configuration API', () => { }); }); - test('correctly applies filters', async () => { + test('should applies correct filters', async () => { await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, @@ -175,7 +204,7 @@ describe('Case Configuration API', () => { }); }); - test('tags with weird chars get handled gracefully', async () => { + test('should handle tags with weird chars', async () => { const weirdTags: string[] = ['(', '"double"']; await getCases({ @@ -204,7 +233,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, @@ -219,7 +248,7 @@ describe('Case Configuration API', () => { fetchMock.mockClear(); fetchMock.mockResolvedValue(casesStatusSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { method: 'GET', @@ -228,7 +257,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(casesStatus); }); @@ -240,7 +269,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(caseUserActionsSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, { method: 'GET', @@ -248,7 +277,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); expect(resp).toEqual(caseUserActions); }); @@ -260,7 +289,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(respReporters); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { method: 'GET', @@ -271,7 +300,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(respReporters); }); @@ -283,7 +312,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(tags); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { method: 'GET', @@ -294,7 +323,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(tags); }); @@ -306,7 +335,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue([basicCaseSnake]); }); const data = { description: 'updated description' }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', @@ -317,7 +346,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchCase( basicCase.id, { description: 'updated description' }, @@ -341,7 +370,7 @@ describe('Case Configuration API', () => { }, ]; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchCasesStatus(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'PATCH', @@ -350,7 +379,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchCasesStatus(data, abortCtrl.signal); expect(resp).toEqual({ ...cases }); }); @@ -362,7 +391,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(basicCaseSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -384,7 +413,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await patchComment({ caseId: basicCase.id, commentId: basicCase.comments[0].id, @@ -418,7 +447,7 @@ describe('Case Configuration API', () => { owner: SECURITY_SOLUTION_OWNER, }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await postCase(data, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { method: 'POST', @@ -427,7 +456,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await postCase(data, abortCtrl.signal); expect(resp).toEqual(basicCase); }); @@ -444,7 +473,7 @@ describe('Case Configuration API', () => { type: CommentType.user as const, }; - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await postComment(data, basicCase.id, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { method: 'POST', @@ -453,7 +482,7 @@ describe('Case Configuration API', () => { }); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await postComment(data, basicCase.id, abortCtrl.signal); expect(resp).toEqual(basicCase); }); @@ -467,7 +496,7 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(pushedCaseSnake); }); - test('check url, method, signal', async () => { + test('should be called with correct check url, method, signal', async () => { await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith( `${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`, @@ -479,7 +508,7 @@ describe('Case Configuration API', () => { ); }); - test('happy path', async () => { + test('should return correct response', async () => { const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(resp).toEqual(pushedCase); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 51a68376936af..75e8c8f58705d 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -14,6 +14,7 @@ import { CasePatchRequest, CasePostRequest, CaseResponse, + CaseResolveResponse, CASES_URL, CasesFindResponse, CasesResponse, @@ -35,6 +36,7 @@ import { SubCaseResponse, SubCasesResponse, User, + ResolvedCase, } from '../../common'; import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api'; @@ -61,6 +63,7 @@ import { decodeCasesFindResponse, decodeCasesStatusResponse, decodeCaseUserActionsResponse, + decodeCaseResolveResponse, } from './utils'; export const getCase = async ( @@ -78,6 +81,24 @@ export const getCase = async ( return convertToCamelCase(decodeCaseResponse(response)); }; +export const resolveCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + getCaseDetailsUrl(caseId) + '/resolve', + { + method: 'GET', + query: { + includeComments, + }, + signal, + } + ); + return convertToCamelCase(decodeCaseResolveResponse(response)); +}; + export const getSubCase = async ( caseId: string, subCaseId: string, diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index fcd564969d486..f7d1daabd60ea 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -20,6 +20,7 @@ import { CommentResponse, CommentType, ConnectorTypes, + ResolvedCase, isCreateConnector, isPush, isUpdateConnector, @@ -163,6 +164,12 @@ export const basicCase: Case = { subCaseIds: [], }; +export const basicResolvedCase: ResolvedCase = { + case: basicCase, + outcome: 'aliasMatch', + aliasTargetId: `${basicCase.id}_2`, +}; + export const collectionCase: Case = { type: CaseType.collection, owner: SECURITY_SOLUTION_OWNER, diff --git a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx index c88f530709c8a..e825e232aebdc 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useGetCase, UseGetCase } from './use_get_case'; -import { basicCase } from './mock'; +import { basicCase, basicResolvedCase } from './mock'; import * as api from './api'; jest.mock('./api'); @@ -28,6 +28,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ data: null, + resolveOutcome: null, isLoading: false, isError: false, fetchCase: result.current.fetchCase, @@ -36,13 +37,13 @@ describe('useGetCase', () => { }); }); - it('calls getCase with correct arguments', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); + it('calls resolveCase with correct arguments', async () => { + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); await act(async () => { const { waitForNextUpdate } = renderHook(() => useGetCase(basicCase.id)); await waitForNextUpdate(); await waitForNextUpdate(); - expect(spyOnGetCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); + expect(spyOnResolveCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal); }); }); @@ -55,6 +56,8 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ data: basicCase, + resolveOutcome: basicResolvedCase.outcome, + resolveAliasId: basicResolvedCase.aliasTargetId, isLoading: false, isError: false, fetchCase: result.current.fetchCase, @@ -64,7 +67,7 @@ describe('useGetCase', () => { }); it('refetch case', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetCase(basicCase.id) @@ -72,7 +75,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); await waitForNextUpdate(); result.current.fetchCase(); - expect(spyOnGetCase).toHaveBeenCalledTimes(2); + expect(spyOnResolveCase).toHaveBeenCalledTimes(2); }); }); @@ -103,8 +106,8 @@ describe('useGetCase', () => { }); it('unhappy path', async () => { - const spyOnGetCase = jest.spyOn(api, 'getCase'); - spyOnGetCase.mockImplementation(() => { + const spyOnResolveCase = jest.spyOn(api, 'resolveCase'); + spyOnResolveCase.mockImplementation(() => { throw new Error('Something went wrong'); }); @@ -117,6 +120,7 @@ describe('useGetCase', () => { expect(result.current).toEqual({ data: null, + resolveOutcome: null, isLoading: false, isError: true, fetchCase: result.current.fetchCase, diff --git a/x-pack/plugins/cases/public/containers/use_get_case.tsx b/x-pack/plugins/cases/public/containers/use_get_case.tsx index b9326ad057c9e..52610981a227c 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case.tsx @@ -7,20 +7,22 @@ import { useEffect, useReducer, useCallback, useRef } from 'react'; -import { Case } from './types'; +import { Case, ResolvedCase } from './types'; import * as i18n from './translations'; import { useToasts } from '../common/lib/kibana'; -import { getCase, getSubCase } from './api'; +import { resolveCase, getSubCase } from './api'; interface CaseState { data: Case | null; + resolveOutcome: ResolvedCase['outcome'] | null; + resolveAliasId?: string; isLoading: boolean; isError: boolean; } type Action = | { type: 'FETCH_INIT'; payload: { silent: boolean } } - | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_SUCCESS'; payload: ResolvedCase } | { type: 'FETCH_FAILURE' } | { type: 'UPDATE_CASE'; payload: Case }; @@ -40,7 +42,9 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { ...state, isLoading: false, isError: false, - data: action.payload, + data: action.payload.case, + resolveOutcome: action.payload.outcome, + resolveAliasId: action.payload.aliasTargetId, }; case 'FETCH_FAILURE': return { @@ -72,6 +76,7 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { isLoading: false, isError: false, data: null, + resolveOutcome: null, }); const toasts = useToasts(); const isCancelledRef = useRef(false); @@ -89,9 +94,12 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: { silent } }); - const response = await (subCaseId - ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) - : getCase(caseId, true, abortCtrlRef.current.signal)); + const response: ResolvedCase = subCaseId + ? { + case: await getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal), + outcome: 'exactMatch', // sub-cases are not resolved, forced to exactMatch always + } + : await resolveCase(caseId, true, abortCtrlRef.current.signal); if (!isCancelledRef.current) { dispatch({ type: 'FETCH_SUCCESS', payload: response }); diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index b0cc0c72fee78..458899e5f53c9 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -30,6 +30,8 @@ import { CaseUserActionsResponseRt, CommentType, CasePatchRequest, + CaseResolveResponse, + CaseResolveResponseRt, } from '../../common'; import { AllCases, Case, UpdateByKey } from './types'; import * as i18n from './translations'; @@ -80,6 +82,12 @@ export const createToasterPlainError = (message: string) => new ToasterError([me export const decodeCaseResponse = (respCase?: CaseResponse) => pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); +export const decodeCaseResolveResponse = (respCase?: CaseResolveResponse) => + pipe( + CaseResolveResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + export const decodeCasesResponse = (respCase?: CasesResponse) => pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index db2e5d6ab6bff..5b19bcfa8ac46 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -16,6 +16,7 @@ import type { } from '../../triggers_actions_ui/public'; import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { SpacesPluginStart } from '../../spaces/public'; import type { Storage } from '../../../../src/plugins/kibana_utils/public'; import { AllCasesProps } from './components/all_cases'; @@ -36,6 +37,7 @@ export interface StartPlugins { lens: LensPublicStart; storage: Storage; triggersActionsUi: TriggersActionsStart; + spaces?: SpacesPluginStart; } /** diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index 3ca77944776b3..50c085b7f22a8 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -1596,6 +1596,90 @@ Object { } `; +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_resolve", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error and entity 1`] = ` Object { "error": Object { diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 90b89c7f75766..1a74640515173 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -152,6 +152,14 @@ export const Operations: Record Promise; */ export enum ReadOperations { GetCase = 'getCase', + ResolveCase = 'resolveCase', FindCases = 'findCases', GetCaseIDsByAlertID = 'getCaseIDsByAlertID', GetCaseStatuses = 'getCaseStatuses', diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 0932308c2e269..fd9bd489f31b2 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -18,6 +18,7 @@ import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; import { ICasePostRequest, + ICaseResolveResponse, ICaseResponse, ICasesFindRequest, ICasesFindResponse, @@ -31,6 +32,7 @@ import { find } from './find'; import { CasesByAlertIDParams, get, + resolve, getCasesByAlertID, GetParams, getReporters, @@ -57,6 +59,11 @@ export interface CasesSubClient { * Retrieves a single case with the specified ID. */ get(params: GetParams): Promise; + /** + * @experimental + * Retrieves a single case resolving the specified ID. + */ + resolve(params: GetParams): Promise; /** * Pushes a specific case to an external system. */ @@ -99,6 +106,7 @@ export const createCasesSubClient = ( create: (data: CasePostRequest) => create(data, clientArgs), find: (params: CasesFindRequest) => find(params, clientArgs), get: (params: GetParams) => get(params, clientArgs), + resolve: (params: GetParams) => resolve(params, clientArgs), push: (params: PushParams) => push(params, clientArgs, casesClient, casesClientInternal), update: (cases: CasesPatchRequest) => update(cases, clientArgs, casesClientInternal), delete: (ids: string[]) => deleteCases(ids, clientArgs), diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 6b0015d4ffb14..c6ab033c2a848 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -9,10 +9,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject } from 'kibana/server'; +import { SavedObject, SavedObjectsResolveResponse } from 'kibana/server'; import { CaseResponseRt, CaseResponse, + CaseResolveResponseRt, + CaseResolveResponse, User, UsersRt, AllTagsFindRequest, @@ -230,6 +232,86 @@ export const get = async ( } }; +/** + * Retrieves a case resolving its ID and optionally loading its comments and sub case comments. + * + * @experimental + */ +export const resolve = async ( + { id, includeComments, includeSubCaseComments }: GetParams, + clientArgs: CasesClientArgs +): Promise => { + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; + + try { + if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { + throw Boom.badRequest( + 'The `includeSubCaseComments` is not supported when the case connector feature is disabled' + ); + } + + const { + saved_object: savedObject, + ...resolveData + }: SavedObjectsResolveResponse = await caseService.getResolveCase({ + unsecuredSavedObjectsClient, + id, + }); + + await authorization.ensureAuthorized({ + operation: Operations.resolveCase, + entities: [ + { + id: savedObject.id, + owner: savedObject.attributes.owner, + }, + ], + }); + + let subCaseIds: string[] = []; + if (ENABLE_CASE_CONNECTOR) { + const subCasesForCaseId = await caseService.findSubCasesByCaseId({ + unsecuredSavedObjectsClient, + ids: [id], + }); + subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + } + + if (!includeComments) { + return CaseResolveResponseRt.encode({ + ...resolveData, + case: flattenCaseSavedObject({ + savedObject, + subCaseIds, + }), + }); + } + + const theComments = await caseService.getAllCaseComments({ + unsecuredSavedObjectsClient, + id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments, + }); + + return CaseResolveResponseRt.encode({ + ...resolveData, + case: flattenCaseSavedObject({ + savedObject, + subCaseIds, + comments: theComments.saved_objects, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ comments: theComments, id }), + }), + }); + } catch (error) { + throw createCaseError({ message: `Failed to resolve case id: ${id}: ${error}`, error, logger }); + } +}; + /** * Retrieves the tags from all the cases. */ diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 05b7d055656b1..f0ca7ae9eaf71 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -22,6 +22,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => { return { create: jest.fn(), find: jest.fn(), + resolve: jest.fn(), get: jest.fn(), push: jest.fn(), update: jest.fn(), diff --git a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts index bf444ee9420ed..feeaa6b6dcb58 100644 --- a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts +++ b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts @@ -16,6 +16,7 @@ import { AllCommentsResponse, CasePostRequest, + CaseResolveResponse, CaseResponse, CasesConfigurePatch, CasesConfigureRequest, @@ -40,6 +41,7 @@ export interface ICasePostRequest extends CasePostRequest {} export interface ICasesFindRequest extends CasesFindRequest {} export interface ICasesPatchRequest extends CasesPatchRequest {} export interface ICaseResponse extends CaseResponse {} +export interface ICaseResolveResponse extends CaseResolveResponse {} export interface ICasesResponse extends CasesResponse {} export interface ICasesFindResponse extends CasesFindResponse {} diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index 2313c3cad9007..4d81b6d5e11b3 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -45,4 +45,38 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { } } ); + + router.get( + { + path: `${CASE_DETAILS_URL}/resolve`, + validate: { + params: schema.object({ + case_id: schema.string(), + }), + query: schema.object({ + includeComments: schema.boolean({ defaultValue: true }), + includeSubCaseComments: schema.maybe(schema.boolean({ defaultValue: false })), + }), + }, + }, + async (context, request, response) => { + try { + const casesClient = await context.cases.getCasesClient(); + const id = request.params.case_id; + + return response.ok({ + body: await casesClient.cases.resolve({ + id, + includeComments: request.query.includeComments, + includeSubCaseComments: request.query.includeSubCaseComments, + }), + }); + } catch (error) { + logger.error( + `Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments} \ninclude sub comments: ${request.query.includeSubCaseComments}: ${error}` + ); + return response.customError(wrapError(error)); + } + } + ); } diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 72c2033f83535..3c76be6d6dd93 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -16,6 +16,7 @@ import { SavedObjectsFindResult, SavedObjectsBulkUpdateResponse, SavedObjectsUpdateResponse, + SavedObjectsResolveResponse, } from 'kibana/server'; import type { estypes } from '@elastic/elasticsearch'; @@ -738,6 +739,27 @@ export class CasesService { throw error; } } + + public async getResolveCase({ + unsecuredSavedObjectsClient, + id: caseId, + }: GetCaseArgs): Promise> { + try { + this.log.debug(`Attempting to resolve case ${caseId}`); + const resolveCaseResult = await unsecuredSavedObjectsClient.resolve( + CASE_SAVED_OBJECT, + caseId + ); + return { + ...resolveCaseResult, + saved_object: transformSavedObjectToExternalModel(resolveCaseResult.saved_object), + }; + } catch (error) { + this.log.error(`Error on resolve case ${caseId}: ${error}`); + throw error; + } + } + public async getSubCase({ unsecuredSavedObjectsClient, id, diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index a29d227cfbb0f..1ea9f481d302f 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -36,6 +36,7 @@ export const createCaseServiceMock = (): CaseServiceMock => { getCases: jest.fn(), getCaseIdsByAlertId: jest.fn(), getMostRecentSubCase: jest.fn(), + getResolveCase: jest.fn(), getSubCase: jest.fn(), getSubCases: jest.fn(), getTags: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index 3fbae0a564c17..2ecb6bffc8212 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -69,13 +69,13 @@ export const ErrorStatePrompt: React.FC = () => {
  • diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 5f515fc99769c..28e796c256396 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -25,6 +25,7 @@ export const contentSources = [ allowsReauth: true, boost: 1, activities: [], + isOauth1: false, }, { id: '124', @@ -40,6 +41,7 @@ export const contentSources = [ allowsReauth: true, boost: 0.5, activities: [], + isOauth1: true, }, ]; @@ -303,6 +305,7 @@ export const sourceConfigData = { privateSourcesEnabled: false, categories: ['wiki', 'atlassian', 'intranet'], configuredFields: { + isOauth1: false, clientId: 'CyztADsSECRETCSAUCEh1a', clientSecret: 'GSjJxqSECRETCSAUCEksHk', baseUrl: 'https://mine.atlassian.net', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx index 9af91107d7304..9aa0286b2bef0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx @@ -25,6 +25,7 @@ describe('SourceConfigFields', () => { it('renders with all items, hiding API Keys', () => { const wrapper = shallow( { it('shows API keys', () => { const wrapper = shallow( - + ); expect(wrapper.find(ApiKey)).toHaveLength(2); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx index 236d475b8f687..e33e7817b5209 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx @@ -20,6 +20,7 @@ import { ApiKey } from '../api_key'; import { CredentialItem } from '../credential_item'; interface SourceConfigFieldsProps { + isOauth1?: boolean; clientId?: string; clientSecret?: string; publicKey?: string; @@ -28,14 +29,13 @@ interface SourceConfigFieldsProps { } export const SourceConfigFields: React.FC = ({ + isOauth1, clientId, clientSecret, publicKey, consumerKey, baseUrl, }) => { - const showApiKey = (publicKey || consumerKey) && !clientId; - const credentialItem = (label: string, item?: string) => item && ; @@ -58,10 +58,10 @@ export const SourceConfigFields: React.FC = ({ return ( <> - {showApiKey && keyElement} - {credentialItem(CLIENT_ID_LABEL, clientId)} + {isOauth1 && keyElement} + {!isOauth1 && credentialItem(CLIENT_ID_LABEL, clientId)} - {credentialItem(CLIENT_SECRET_LABEL, clientSecret)} + {!isOauth1 && credentialItem(CLIENT_SECRET_LABEL, clientSecret)} {credentialItem(BASE_URL_LABEL, baseUrl)} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index d8fcb414cff75..c524bd4f7617a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -108,6 +108,7 @@ export interface ContentSourceDetails extends ContentSource { allowsReauth: boolean; boost: number; activities: SourceActivity[]; + isOauth1: boolean; } interface DescriptionList { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 6b3d126ec8c0b..585477fed058e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -77,6 +77,7 @@ export const SourceSettings: React.FC = () => { custom: isCustom, isIndexedSource, areThumbnailsConfigEnabled, + isOauth1, indexing: { enabled, features: { @@ -98,10 +99,9 @@ export const SourceSettings: React.FC = () => { getSourceConfigData(serviceType); }, []); - const { - configuration: { isPublicKey }, - editPath, - } = staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem; + const { editPath } = staticSourceData.find( + (source) => source.serviceType === serviceType + ) as SourceDataItem; const [inputValue, setValue] = useState(name); const [confirmModalVisible, setModalVisibility] = useState(false); @@ -207,10 +207,11 @@ export const SourceSettings: React.FC = () => { {showConfig && ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index d3a6bb7561d39..9f125533f36c2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -102,6 +102,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { field: 'name', sortable: true, + truncateText: true, name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { defaultMessage: 'Name', }), diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 31a3e2164a247..d70b6c68016be 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -241,38 +241,6 @@ describe('when on integration detail', () => { 'http://localhost/mock/app/integrations/edit-integration/e8a37031-2907-44f6-89d2-98bd493f60dc' ); }); - - it('should NOT show link for agent count if it is zero', async () => { - await mockedApi.waitForApi(); - const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; - expect(firstRowAgentCount.textContent).toEqual('0'); - expect(firstRowAgentCount.tagName).not.toEqual('A'); - }); - - it('should show add agent button if agent count is zero', async () => { - await mockedApi.waitForApi(); - const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; - expect(firstRowAgentCount.textContent).toEqual('0'); - - const addAgentButton = renderResult.getAllByTestId('addAgentButton')[0]; - expect(addAgentButton).not.toBeNull(); - }); - - it('should show link for agent count if greater than zero', async () => { - await mockedApi.waitForApi(); - const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; - expect(secondRowAgentCount.textContent).toEqual('100'); - expect(secondRowAgentCount.tagName).toEqual('A'); - }); - - it('should NOT show add agent button if agent count is greater than zero', async () => { - await mockedApi.waitForApi(); - const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; - expect(secondRowAgentCount.textContent).toEqual('100'); - - const addAgentButton = renderResult.getAllByTestId('addAgentButton')[1]; - expect(addAgentButton).toBeUndefined(); - }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.tsx new file mode 100644 index 0000000000000..8872c61299093 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { act } from '@testing-library/react'; + +import { createIntegrationsTestRendererMock } from '../../../../../../../../mock'; + +import { PackagePolicyAgentsCell } from './package_policy_agents_cell'; + +function renderCell({ agentCount = 0, agentPolicyId = '123', onAddAgent = () => {} }) { + const renderer = createIntegrationsTestRendererMock(); + + return renderer.render( + + ); +} + +describe('PackagePolicyAgentsCell', () => { + test('it should display add agent if count is 0', async () => { + const utils = renderCell({ agentCount: 0 }); + await act(async () => { + expect(utils.queryByText('Add agent')).toBeInTheDocument(); + }); + }); + + test('it should display only count if count > 0', async () => { + const utils = renderCell({ agentCount: 9999 }); + await act(async () => { + expect(utils.queryByText('Add agent')).not.toBeInTheDocument(); + expect(utils.queryByText('9999')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx new file mode 100644 index 0000000000000..37543e7e5ae1b --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/package_policy_agents_cell.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { LinkedAgentCount } from '../../../../../../components'; + +export const PackagePolicyAgentsCell = ({ + agentPolicyId, + agentCount = 0, + onAddAgent, +}: { + agentPolicyId: string; + agentCount?: number; + onAddAgent: () => void; +}) => { + if (agentCount > 0) { + return ( + + ); + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 92b4012011fc8..42eb68099970a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -13,19 +13,16 @@ import type { EuiTableFieldDataColumnType, } from '@elastic/eui'; import { - EuiButtonIcon, EuiBasicTable, EuiLink, EuiFlexGroup, EuiFlexItem, - EuiToolTip, EuiText, EuiButton, EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; import { InstallStatus } from '../../../../../types'; import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; @@ -41,10 +38,10 @@ import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { AgentEnrollmentFlyout, AgentPolicySummaryLine, - LinkedAgentCount, PackagePolicyActionsMenu, } from '../../../../../components'; +import { PackagePolicyAgentsCell } from './components/package_policy_agents_cell'; import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; import { Persona } from './persona'; @@ -58,10 +55,6 @@ interface InMemoryPackagePolicyAndAgentPolicy { agentPolicy: GetAgentPoliciesResponseItem; } -const AddAgentButton = styled(EuiButtonIcon)` - margin-left: ${(props) => props.theme.eui.euiSizeS}; -`; - const IntegrationDetailsLink = memo<{ packagePolicy: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']; }>(({ packagePolicy }) => { @@ -266,51 +259,6 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps return ; }, }, - { - field: '', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { - defaultMessage: 'Agents', - }), - truncateText: true, - align: 'left', - width: '8ch', - render({ packagePolicy, agentPolicy }: InMemoryPackagePolicyAndAgentPolicy) { - const count = agentPolicy?.agents ?? 0; - - return ( - <> - - {count === 0 && ( - - setFlyoutOpenForPolicyId(agentPolicy.id)} - data-test-subj="addAgentButton" - aria-label={i18n.translate( - 'xpack.fleet.epm.packageDetails.integrationList.addAgent', - { - defaultMessage: 'Add Agent', - } - )} - /> - - )} - - ); - }, - }, { field: 'packagePolicy.updated_by', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { @@ -335,6 +283,21 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); }, }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { + defaultMessage: 'Agents', + }), + render({ agentPolicy }: InMemoryPackagePolicyAndAgentPolicy) { + return ( + setFlyoutOpenForPolicyId(agentPolicy.id)} + /> + ); + }, + }, { field: '', name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 7dc313970ef20..ee7d5f97fcbac 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -69,7 +69,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ > , ] diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index a7cf606e92c0b..b3197d918d231 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -136,7 +136,7 @@ export const createAgentPolicyHandler: RequestHandler< }); } - await agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id); + await agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); const body: CreateAgentPolicyResponse = { item: agentPolicy, diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 0e22f544ddfa3..bd82989a9e828 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -11,17 +11,17 @@ import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { appContextService } from '../../services/app_context'; -import { setupIngestManager } from '../../services/setup'; +import { setupFleet } from '../../services/setup'; import { fleetSetupHandler } from './handlers'; jest.mock('../../services/setup', () => { return { - setupIngestManager: jest.fn(), + setupFleet: jest.fn(), }; }); -const mockSetupIngestManager = setupIngestManager as jest.MockedFunction; +const mockSetupFleet = setupFleet as jest.MockedFunction; describe('FleetSetupHandler', () => { let context: ReturnType; @@ -45,7 +45,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup succeeds w/200 and body of resolved value', async () => { - mockSetupIngestManager.mockImplementation(() => + mockSetupFleet.mockImplementation(() => Promise.resolve({ isInitialized: true, nonFatalErrors: [], @@ -59,9 +59,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup fails w/500 on custom error', async () => { - mockSetupIngestManager.mockImplementation(() => - Promise.reject(new Error('SO method mocked to throw')) - ); + mockSetupFleet.mockImplementation(() => Promise.reject(new Error('SO method mocked to throw'))); await fleetSetupHandler(context, request, response); expect(response.customError).toHaveBeenCalledTimes(1); @@ -74,7 +72,7 @@ describe('FleetSetupHandler', () => { }); it('POST /setup fails w/502 on RegistryError', async () => { - mockSetupIngestManager.mockImplementation(() => + mockSetupFleet.mockImplementation(() => Promise.reject(new RegistryError('Registry method mocked to throw')) ); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index fe1e30f9f05d6..6311b9d970d35 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -9,7 +9,7 @@ import type { RequestHandler } from 'src/core/server'; import { appContextService } from '../../services'; import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../common'; -import { setupIngestManager } from '../../services/setup'; +import { setupFleet } from '../../services/setup'; import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; @@ -46,7 +46,7 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon try { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupStatus = await setupIngestManager(soClient, esClient); + const setupStatus = await setupFleet(soClient, esClient); const body: PostFleetSetupResponse = { ...setupStatus, nonFatalErrors: setupStatus.nonFatalErrors.map((e) => { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 6a5cb28dbaa0a..5617f8ef7bd7c 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -7,13 +7,16 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; -import type { AgentPolicy, NewAgentPolicy } from '../types'; +import type { AgentPolicy, FullAgentPolicy, NewAgentPolicy } from '../types'; import { agentPolicyService } from './agent_policy'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; +import { appContextService } from './app_context'; +import { outputService } from './output'; +import { getFullAgentPolicy } from './agent_policies'; function getSavedObjectMock(agentPolicyAttributes: any) { const mock = savedObjectsClientMock.create(); @@ -47,9 +50,18 @@ function getSavedObjectMock(agentPolicyAttributes: any) { return mock; } +jest.mock('./output'); jest.mock('./agent_policy_update'); jest.mock('./agents'); jest.mock('./package_policy'); +jest.mock('./app_context'); +jest.mock('./agent_policies/full_agent_policy'); + +const mockedAppContextService = appContextService as jest.Mocked; +const mockedOutputService = outputService as jest.Mocked; +const mockedGetFullAgentPolicy = getFullAgentPolicy as jest.Mock< + ReturnType +>; function getAgentPolicyUpdateMock() { return agentPolicyUpdateEventHandler as unknown as jest.Mock< @@ -214,4 +226,64 @@ describe('agent policy', () => { expect(calledWith[2]).toHaveProperty('is_managed', true); }); }); + + describe('createFleetServerPolicy', () => { + beforeEach(() => { + mockedGetFullAgentPolicy.mockReset(); + }); + it('should not create a .fleet-policy document if we cannot get the full policy', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedGetFullAgentPolicy.mockResolvedValue(null); + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'policy123', + type: 'mocked', + references: [], + }); + await agentPolicyService.createFleetServerPolicy(soClient, 'policy123'); + + expect(esClient.create).not.toBeCalled(); + }); + + it('should create a .fleet-policy document if we can get the full policy', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient); + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output'); + mockedGetFullAgentPolicy.mockResolvedValue({ + id: 'policy123', + revision: 1, + inputs: [ + { + id: 'input-123', + }, + ], + } as FullAgentPolicy); + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'policy123', + type: 'mocked', + references: [], + }); + await agentPolicyService.createFleetServerPolicy(soClient, 'policy123'); + + expect(esClient.create).toBeCalledWith( + expect.objectContaining({ + index: '.fleet-policies', + body: expect.objectContaining({ + '@timestamp': expect.anything(), + data: { id: 'policy123', inputs: [{ id: 'input-123' }], revision: 1 }, + default_fleet_server: false, + policy_id: 'policy123', + revision_idx: 1, + }), + }) + ); + }); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 751e981cb8085..6ebe890aeaef2 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -429,7 +429,7 @@ class AgentPolicyService { throw new Error('Copied agent policy not found'); } - await this.createFleetPolicyChangeAction(soClient, newAgentPolicy.id); + await this.createFleetServerPolicy(soClient, newAgentPolicy.id); return updatedAgentPolicy; } @@ -655,10 +655,11 @@ class AgentPolicyService { }; } - public async createFleetPolicyChangeAction( + public async createFleetServerPolicy( soClient: SavedObjectsClientContract, agentPolicyId: string ) { + // Use internal ES client so we have permissions to write to .fleet* indices const esClient = appContextService.getInternalUserESClient(); const defaultOutputId = await outputService.getDefaultOutputId(soClient); @@ -666,14 +667,6 @@ class AgentPolicyService { return; } - await this.createFleetPolicyChangeFleetServer(soClient, esClient, agentPolicyId); - } - - public async createFleetPolicyChangeFleetServer( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - agentPolicyId: string - ) { const policy = await agentPolicyService.get(soClient, agentPolicyId); const fullPolicy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); if (!policy || !fullPolicy || !fullPolicy.revision) { diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts index 9703467d84c18..51bf068b8b111 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts @@ -43,11 +43,11 @@ export async function agentPolicyUpdateEventHandler( name: 'Default', agentPolicyId, }); - await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); + await agentPolicyService.createFleetServerPolicy(internalSoClient, agentPolicyId); } if (action === 'updated') { - await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); + await agentPolicyService.createFleetServerPolicy(internalSoClient, agentPolicyId); } if (action === 'deleted') { diff --git a/x-pack/plugins/fleet/server/services/agents/setup.ts b/x-pack/plugins/fleet/server/services/agents/setup.ts index 81ae6b177783d..2b680dee1146e 100644 --- a/x-pack/plugins/fleet/server/services/agents/setup.ts +++ b/x-pack/plugins/fleet/server/services/agents/setup.ts @@ -11,12 +11,9 @@ import { SO_SEARCH_LIMIT } from '../../constants'; import { agentPolicyService } from '../agent_policy'; /** - * During the migration from 7.9 to 7.10 we introduce a new agent action POLICY_CHANGE per policy - * this function ensure that action exist for each policy - * - * @param soClient + * Ensure a .fleet-policy document exist for each agent policy so Fleet server can retrieve it */ -export async function ensureAgentActionPolicyChangeExists( +export async function ensureFleetServerAgentPoliciesExists( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ) { @@ -32,7 +29,7 @@ export async function ensureAgentActionPolicyChangeExists( )); if (!policyChangeActionExist) { - return agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id); + return agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); } }) ); diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts index 01fd4ad143d18..61f6cc164eb30 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.test.ts @@ -15,7 +15,7 @@ const { Response, FetchError } = jest.requireActual('node-fetch'); const fetchMock = require('node-fetch') as jest.Mock; jest.setTimeout(120 * 1000); -describe('setupIngestManager', () => { +describe('Registry request', () => { beforeEach(async () => {}); afterEach(async () => { diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 379bc8fa39bff..bbaf9c9479eb4 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -190,11 +190,7 @@ async function migrateAgentPolicies() { // @ts-expect-error value is number | TotalHits if (res.body.hits.total.value === 0) { - return agentPolicyService.createFleetPolicyChangeFleetServer( - soClient, - esClient, - agentPolicy.id - ); + return agentPolicyService.createFleetServerPolicy(soClient, agentPolicy.id); } }) ); diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index 212b0fabd26fb..e6b76694a9fca 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -8,7 +8,7 @@ import { createAppContextStartContractMock, xpackMocks } from '../mocks'; import { appContextService } from './app_context'; -import { setupIngestManager } from './setup'; +import { setupFleet } from './setup'; const mockedMethodThrowsError = () => jest.fn().mockImplementation(() => { @@ -21,7 +21,7 @@ const mockedMethodThrowsCustom = () => throw new CustomTestError('method mocked to throw'); }); -describe('setupIngestManager', () => { +describe('setupFleet', () => { let context: ReturnType; beforeEach(async () => { @@ -44,7 +44,7 @@ describe('setupIngestManager', () => { soClient.update = mockedMethodThrowsError(); const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, esClient); + const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('SO method mocked to throw'); await expect(setupPromise).rejects.toThrow(Error); }); @@ -57,7 +57,7 @@ describe('setupIngestManager', () => { soClient.update = mockedMethodThrowsCustom(); const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, esClient); + const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('method mocked to throw'); await expect(setupPromise).rejects.toThrow(CustomTestError); }); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 8c49bffdbf25c..08c580d80c804 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -25,7 +25,7 @@ import { outputService } from './output'; import { generateEnrollmentAPIKey, hasEnrollementAPIKeysForPolicy } from './api_keys'; import { settingsService } from '.'; import { awaitIfPending } from './setup_utils'; -import { ensureAgentActionPolicyChangeExists } from './agents'; +import { ensureFleetServerAgentPoliciesExists } from './agents'; import { awaitIfFleetServerSetupPending } from './fleet_server'; import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install'; import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install'; @@ -38,7 +38,7 @@ export interface SetupStatus { nonFatalErrors: Array; } -export async function setupIngestManager( +export async function setupFleet( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient ): Promise { @@ -101,7 +101,7 @@ async function createSetupSideEffects( await cleanPreconfiguredOutputs(soClient, outputsOrUndefined ?? []); await ensureDefaultEnrollmentAPIKeysExists(soClient, esClient); - await ensureAgentActionPolicyChangeExists(soClient, esClient); + await ensureFleetServerAgentPoliciesExists(soClient, esClient); return { isInitialized: true, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index c7c1eb5454d1d..45eef3cc85a57 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -64,6 +64,7 @@ export const evaluateAlert = { const { criteria, groupBy, filterQuery, shouldDropPartialBuckets } = params; @@ -91,21 +92,53 @@ export const evaluateAlert = { - if (isTooManyBucketsPreviewException(points)) throw points; - return { - ...criterion, - metric: criterion.metric ?? DOCUMENT_COUNT_I18N, - currentValue: Array.isArray(points) ? last(points)?.value : NaN, - timestamp: Array.isArray(points) ? last(points)?.key : NaN, - shouldFire: pointsEvaluator(points, threshold, comparator), - shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator), - isNoData: Array.isArray(points) - ? points.map((point) => point?.value === null || point === null) - : [points === null], - isError: isNaN(Array.isArray(points) ? last(points)?.value : points), - }; - }); + // If any previous groups are no longer being reported, backfill them with null values + const currentGroups = Object.keys(currentValues); + + const missingGroups = prevGroups.filter((g) => !currentGroups.includes(g)); + if (currentGroups.length === 0 && missingGroups.length === 0) { + missingGroups.push(UNGROUPED_FACTORY_KEY); + } + const backfillTimestamp = + last(last(Object.values(currentValues)))?.key ?? new Date().toISOString(); + const backfilledPrevGroups: Record< + string, + Array<{ key: string; value: number }> + > = missingGroups.reduce( + (result, group) => ({ + ...result, + [group]: [ + { + key: backfillTimestamp, + value: criterion.aggType === Aggregators.COUNT ? 0 : null, + }, + ], + }), + {} + ); + const currentValuesWithBackfilledPrevGroups = { + ...currentValues, + ...backfilledPrevGroups, + }; + + return mapValues( + currentValuesWithBackfilledPrevGroups, + (points: any[] | typeof NaN | null) => { + if (isTooManyBucketsPreviewException(points)) throw points; + return { + ...criterion, + metric: criterion.metric ?? DOCUMENT_COUNT_I18N, + currentValue: Array.isArray(points) ? last(points)?.value : NaN, + timestamp: Array.isArray(points) ? last(points)?.key : NaN, + shouldFire: pointsEvaluator(points, threshold, comparator), + shouldWarn: pointsEvaluator(points, warningThreshold, warningComparator), + isNoData: Array.isArray(points) + ? points.map((point) => point?.value === null || point === null) + : [points === null], + isError: isNaN(Array.isArray(points) ? last(points)?.value : points), + }; + } + ); }) ); }; @@ -119,7 +152,7 @@ const getMetric: ( filterQuery: string | undefined, timeframe?: { start?: number; end: number }, shouldDropPartialBuckets?: boolean -) => Promise> = async function ( +) => Promise>> = async function ( esClient, params, index, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 8eb19ad582057..869d0afd52367 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -37,10 +37,13 @@ let persistAlertInstances = false; // eslint-disable-line prefer-const type TestRuleState = Record & { aRuleStateKey: string; + groups: string[]; + groupBy?: string | string[]; }; const initialRuleState: TestRuleState = { aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', + groups: [], }; const mockOptions = { @@ -90,6 +93,7 @@ const mockOptions = { describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -157,20 +161,29 @@ describe('The metric threshold alert type', () => { }); describe('querying with a groupBy parameter', () => { - const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => + afterAll(() => clearInstances()); + const execute = ( + comparator: Comparator, + threshold: number[], + groupBy: string[] = ['something'], + metric?: string, + state?: any + ) => executor({ ...mockOptions, services, params: { - groupBy: 'something', + groupBy, criteria: [ { ...baseNonCountCriterion, comparator, threshold, + metric: metric ?? baseNonCountCriterion.metric, }, ], }, + state: state ?? mockOptions.state.wrapped, }); const instanceIdA = 'a'; const instanceIdB = 'b'; @@ -194,9 +207,35 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceIdA).action.group).toBe('a'); expect(mostRecentAction(instanceIdB).action.group).toBe('b'); }); + test('reports previous groups and the groupBy parameter in its state', async () => { + const stateResult = await execute(Comparator.GT, [0.75]); + expect(stateResult.groups).toEqual(expect.arrayContaining(['a', 'b'])); + expect(stateResult.groupBy).toEqual(['something']); + }); + test('persists previous groups that go missing, until the groupBy param changes', async () => { + const stateResult1 = await execute(Comparator.GT, [0.75], ['something'], 'test.metric.2'); + expect(stateResult1.groups).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + const stateResult2 = await execute( + Comparator.GT, + [0.75], + ['something'], + 'test.metric.1', + stateResult1 + ); + expect(stateResult2.groups).toEqual(expect.arrayContaining(['a', 'b', 'c'])); + const stateResult3 = await execute( + Comparator.GT, + [0.75], + ['something', 'something-else'], + 'test.metric.1', + stateResult2 + ); + expect(stateResult3.groups).toEqual(expect.arrayContaining(['a', 'b'])); + }); }); describe('querying with multiple criteria', () => { + afterAll(() => clearInstances()); const execute = ( comparator: Comparator, thresholdA: number[], @@ -257,6 +296,7 @@ describe('The metric threshold alert type', () => { }); }); describe('querying with the count aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -279,8 +319,47 @@ describe('The metric threshold alert type', () => { await execute(Comparator.LT, [0.5]); expect(mostRecentAction(instanceID)).toBe(undefined); }); + describe('with a groupBy parameter', () => { + const executeGroupBy = ( + comparator: Comparator, + threshold: number[], + sourceId: string = 'default', + state?: any + ) => + executor({ + ...mockOptions, + services, + params: { + sourceId, + groupBy: 'something', + criteria: [ + { + ...baseCountCriterion, + comparator, + threshold, + }, + ], + }, + state: state ?? mockOptions.state.wrapped, + }); + const instanceIdA = 'a'; + const instanceIdB = 'b'; + + test('successfully detects and alerts on a document count of 0', async () => { + const resultState = await executeGroupBy(Comparator.LT_OR_EQ, [0]); + expect(mostRecentAction(instanceIdA)).toBe(undefined); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + await executeGroupBy(Comparator.LT_OR_EQ, [0], 'empty-response', resultState); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + await executeGroupBy(Comparator.LT_OR_EQ, [0]); + expect(mostRecentAction(instanceIdA)).toBe(undefined); + expect(mostRecentAction(instanceIdB)).toBe(undefined); + }); + }); }); describe('querying with the p99 aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -306,6 +385,7 @@ describe('The metric threshold alert type', () => { }); }); describe('querying with the p95 aggregator', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') => executor({ @@ -332,6 +412,7 @@ describe('The metric threshold alert type', () => { }); }); describe("querying a metric that hasn't reported data", () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (alertOnNoData: boolean, sourceId: string = 'default') => executor({ @@ -360,7 +441,51 @@ describe('The metric threshold alert type', () => { }); }); + describe('querying a groupBy alert that starts reporting no data, and then later reports data', () => { + afterAll(() => clearInstances()); + const instanceID = '*'; + const instanceIdA = 'a'; + const instanceIdB = 'b'; + const execute = (metric: string, state?: any) => + executor({ + ...mockOptions, + services, + params: { + groupBy: 'something', + sourceId: 'default', + criteria: [ + { + ...baseNonCountCriterion, + comparator: Comparator.GT, + threshold: [0], + metric, + }, + ], + alertOnNoData: true, + }, + state: state ?? mockOptions.state.wrapped, + }); + const resultState: any[] = []; + test('first sends a No Data alert with the * group, but then reports groups when data is available', async () => { + resultState.push(await execute('test.metric.3')); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + resultState.push(await execute('test.metric.3', resultState.pop())); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + resultState.push(await execute('test.metric.1', resultState.pop())); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + }); + test('sends No Data alerts for the previously detected groups when they stop reporting data, but not the * group', async () => { + await execute('test.metric.3', resultState.pop()); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(mostRecentAction(instanceIdA).id).toBe(FIRED_ACTIONS.id); + expect(mostRecentAction(instanceIdB).id).toBe(FIRED_ACTIONS.id); + }); + }); + describe("querying a rate-aggregated metric that hasn't reported data", () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = (sourceId: string = 'default') => executor({ @@ -439,6 +564,7 @@ describe('The metric threshold alert type', () => { */ describe('querying a metric with a percentage metric', () => { + afterAll(() => clearInstances()); const instanceID = '*'; const execute = () => executor({ @@ -497,7 +623,15 @@ const services: AlertServicesMock & services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte; if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse(from); + if (params.index === 'empty-response') return mocks.emptyMetricResponse; const metric = params?.body.query.bool.filter[1]?.exists.field; + if (metric === 'test.metric.3') { + return elasticsearchClientMock.createSuccessTransportRequestPromise( + params?.body.aggs.aggregatedIntervals?.aggregations.aggregatedValueMax + ? mocks.emptyRateResponse + : mocks.emptyMetricResponse + ); + } if (params?.body.aggs.groupings) { if (params?.body.aggs.groupings.composite.after) { return elasticsearchClientMock.createSuccessTransportRequestPromise( @@ -517,12 +651,6 @@ services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: a return elasticsearchClientMock.createSuccessTransportRequestPromise( mocks.alternateMetricResponse() ); - } else if (metric === 'test.metric.3') { - return elasticsearchClientMock.createSuccessTransportRequestPromise( - params?.body.aggs.aggregatedIntervals.aggregations.aggregatedValueMax - ? mocks.emptyRateResponse - : mocks.emptyMetricResponse - ); } return elasticsearchClientMock.createSuccessTransportRequestPromise(mocks.basicMetricResponse()); }); @@ -534,6 +662,13 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId type, references: [], }; + if (sourceId === 'empty-response') + return { + id: 'empty', + attributes: { metricAlias: 'empty-response' }, + type, + references: [], + }; return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; }); @@ -561,7 +696,13 @@ services.alertInstanceFactory.mockImplementation((instanceID: string) => { }); function mostRecentAction(id: string) { - return alertInstances.get(id)!.actionQueue.pop(); + const instance = alertInstances.get(id); + if (!instance) return undefined; + return instance.actionQueue.pop(); +} + +function clearInstances() { + alertInstances.clear(); } const baseNonCountCriterion: Pick< diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 9c99ad6bf49e2..f49b281909f4b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { first, last } from 'lodash'; +import { first, last, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { ALERT_REASON } from '@kbn/rule-data-utils'; @@ -24,12 +24,16 @@ import { // buildRecoveredAlertReason, stateToAlertMessage, } from '../common/messages'; +import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { createFormatter } from '../../../../common/formatters'; import { AlertStates, Comparator } from './types'; import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; export type MetricThresholdAlertTypeParams = Record; -export type MetricThresholdAlertTypeState = AlertTypeState; // no specific state used +export type MetricThresholdAlertTypeState = AlertTypeState & { + groups: string[]; + groupBy?: string | string[]; +}; export type MetricThresholdAlertInstanceState = AlertInstanceState; // no specific instace state used export type MetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instace state used @@ -58,7 +62,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => MetricThresholdAlertInstanceContext, MetricThresholdAllowedActionGroups >(async function (options) { - const { services, params } = options; + const { services, params, state } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const { alertWithLifecycle, savedObjectsClient } = services; @@ -80,14 +84,28 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => sourceId || 'default' ); const config = source.configuration; + + const previousGroupBy = state.groupBy; + const prevGroups = isEqual(previousGroupBy, params.groupBy) + ? // Filter out the * key from the previous groups, only include it if it's one of + // the current groups. In case of a groupBy alert that starts out with no data and no + // groups, we don't want to persist the existence of the * alert instance + state.groups?.filter((g) => g !== UNGROUPED_FACTORY_KEY) ?? [] + : []; + const alertResults = await evaluateAlert( services.scopedClusterClient.asCurrentUser, params as EvaluatedAlertParams, - config + config, + prevGroups ); // Because each alert result has the same group definitions, just grab the groups from the first one. - const groups = Object.keys(first(alertResults)!); + const resultGroups = Object.keys(first(alertResults)!); + // Merge the list of currently fetched groups and previous groups, and uniquify them. This is necessary for reporting + // no data results on groups that get removed + const groups = [...new Set([...prevGroups, ...resultGroups])]; + for (const group of groups) { // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every((result) => @@ -169,6 +187,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => }); } } + + return { groups, groupBy: params.groupBy }; }); export const FIRED_ACTIONS = { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index b1173f2d611c8..db6b771e91784 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -199,12 +199,20 @@ export const alternateCompositeResponse = (from: number) => ({ buckets: bucketsA(from), }, }, + { + key: { + groupBy0: 'c', + }, + aggregatedIntervals: { + buckets: bucketsC(from), + }, + }, ], }, }, hits: { total: { - value: 2, + value: 3, }, }, }); diff --git a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts index df97c91aacd04..1ab290796e36d 100644 --- a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts @@ -24,7 +24,7 @@ export const getAllCompositeData = async < const { body: response } = await esClientSearch(options); // Nothing available, return the previous buckets. - if (response.hits.total.value === 0) { + if (response.hits?.total.value === 0) { return previousBuckets; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 3864581317e38..be55000bf374a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -42,11 +42,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { defaultMessage="Pattern used to dissect the specified field. The pattern is defined by the parts of the string to discard. Use a {keyModifier} to alter the dissection behavior." values={{ keyModifier: ( - + {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText.dissectProcessorLink', { @@ -97,7 +93,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { export const Dissect: FunctionComponent = () => { const { services } = useKibana(); - const fieldsConfig = getFieldsConfig(services.documentation.getEsDocsBasePath()); + const fieldsConfig = getFieldsConfig(services.documentation.getDissectKeyModifiersUrl()); return ( <> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx index dfbcfc9566507..1c6292795d587 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/enrich.tsx @@ -139,7 +139,6 @@ const fieldsConfig: FieldsConfig = { export const Enrich: FunctionComponent = () => { const { services } = useKibana(); - const esDocUrl = services.documentation.getEsDocsBasePath(); return ( <> { defaultMessage="Name of the {enrichPolicyLink}." values={{ enrichPolicyLink: ( - + {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.policyNameHelpText.enrichPolicyLink', { defaultMessage: 'enrich policy' } @@ -206,11 +209,7 @@ export const Enrich: FunctionComponent = () => { defaultMessage="Operator used to match the geo-shape of incoming documents to enrich documents. Only used for {geoMatchPolicyLink}." values={{ geoMatchPolicyLink: ( - + {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.enrichForm.shapeRelationFieldHelpText.geoMatchPoliciesLink', { defaultMessage: 'geo-match enrich policies' } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx index 9575e6d690e00..9c3601c368342 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx @@ -28,14 +28,12 @@ const { emptyField, isJsonField } = fieldValidators; const INFERENCE_CONFIG_DOCS = { regression: { - path: 'inference-processor.html#inference-processor-regression-opt', linkLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.regressionLinkLabel', { defaultMessage: 'regression' } ), }, classification: { - path: 'inference-processor.html#inference-processor-classification-opt', linkLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.inferenceForm.inferenceConfigField.classificationLinkLabel', { defaultMessage: 'classification' } @@ -43,27 +41,22 @@ const INFERENCE_CONFIG_DOCS = { }, }; -const getInferenceConfigHelpText = (esDocsBasePath: string): React.ReactNode => { +const getInferenceConfigHelpText = ( + regressionDocsLink: string, + classificationDocsLink: string +): React.ReactNode => { return ( + {INFERENCE_CONFIG_DOCS.regression.linkLabel} ), classification: ( - + {INFERENCE_CONFIG_DOCS.classification.linkLabel} ), @@ -158,7 +151,8 @@ const fieldsConfig: FieldsConfig = { export const Inference: FunctionComponent = () => { const { services } = useKibana(); - const esDocUrl = services.documentation.getEsDocsBasePath(); + const regressionDocsLink = services.documentation.getRegressionUrl(); + const classificationDocsLink = services.documentation.getClassificationUrl(); return ( <> @@ -188,7 +182,7 @@ export const Inference: FunctionComponent = () => { = ({ values={{ learnMoreLink: ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index 8aa165cc502a8..801088b868370 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -13,16 +13,28 @@ export class DocumentationService { private processorsUrl: string = ''; private handlingFailureUrl: string = ''; private putPipelineApiUrl: string = ''; + private simulatePipelineApiUrl: string = ''; + private enrichDataUrl: string = ''; + private geoMatchUrl: string = ''; + private dissectKeyModifiersUrl: string = ''; + private classificationUrl: string = ''; + private regressionUrl: string = ''; public setup(docLinks: DocLinksStart): void { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL, links } = docLinks; const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; - this.ingestNodeUrl = `${links.ingest.pipelines}`; - this.processorsUrl = `${links.ingest.processors}`; - this.handlingFailureUrl = `${links.ingest.pipelineFailure}`; - this.putPipelineApiUrl = `${links.apis.createPipeline}`; + this.ingestNodeUrl = links.ingest.pipelines; + this.processorsUrl = links.ingest.processors; + this.handlingFailureUrl = links.ingest.pipelineFailure; + this.putPipelineApiUrl = links.apis.createPipeline; + this.simulatePipelineApiUrl = links.apis.simulatePipeline; + this.enrichDataUrl = links.ingest.enrich; + this.geoMatchUrl = links.ingest.geoMatch; + this.dissectKeyModifiersUrl = links.ingest.dissectKeyModifiers; + this.classificationUrl = links.ingest.inferenceClassification; + this.regressionUrl = links.ingest.inferenceRegression; } public getEsDocsBasePath() { @@ -44,6 +56,30 @@ export class DocumentationService { public getPutPipelineApiUrl() { return this.putPipelineApiUrl; } + + public getSimulatePipelineApiUrl() { + return this.simulatePipelineApiUrl; + } + + public getEnrichDataUrl() { + return this.enrichDataUrl; + } + + public getGeoMatchUrl() { + return this.geoMatchUrl; + } + + public getDissectKeyModifiersUrl() { + return this.dissectKeyModifiersUrl; + } + + public getClassificationUrl() { + return this.classificationUrl; + } + + public getRegressionUrl() { + return this.regressionUrl; + } } export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts new file mode 100644 index 0000000000000..60264f3657fe3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import { EUI_SORT_ASCENDING } from '../../../common/constants'; +import { euiTableStorageGetter, euiTableStorageSetter } from '../../components/table'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +interface Pagination { + pageSize: number; + initialPageSize: number; + pageIndex: number; + initialPageIndex: number; + pageSizeOptions: number[]; + totalItemCount: number; +} + +interface Page { + size: number; + index: number; +} + +interface Sorting { + sort: { + field: string; + direction: string; + }; +} + +const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; + +const DEFAULT_PAGINATION = { + pageSize: 20, + initialPageSize: 20, + pageIndex: 0, + initialPageIndex: 0, + pageSizeOptions: PAGE_SIZE_OPTIONS, + totalItemCount: 0, +}; + +const getPaginationInitialState = (page: Page | undefined) => { + const pagination = DEFAULT_PAGINATION; + + if (page) { + pagination.initialPageSize = page.size; + pagination.pageSize = page.size; + pagination.initialPageIndex = page.index; + pagination.pageIndex = page.index; + } + + return { + ...pagination, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }; +}; + +export function useTable(storageKey: string) { + const storage = new Storage(window.localStorage); + const getLocalStorageData = euiTableStorageGetter(storageKey); + const setLocalStorageData = euiTableStorageSetter(storageKey); + + const storageData = getLocalStorageData(storage); + // get initial state from localstorage + const [pagination, setPagination] = useState( + getPaginationInitialState(storageData.page) + ); + + const updateTotalItemCount = useCallback( + (num) => { + // only update pagination state if different + if (num === pagination.totalItemCount) return; + setPagination({ + ...pagination, + totalItemCount: num, + }); + }, + [setPagination, pagination] + ); + + // get initial state from localStorage + const [sorting, setSorting] = useState(storageData.sort || { sort: {} }); + const cleanSortingData = (sortData: Sorting) => { + const sort = sortData || { sort: {} }; + + if (!sort.sort.field) { + sort.sort.field = 'name'; + } + if (!sort.sort.direction) { + sort.sort.direction = EUI_SORT_ASCENDING; + } + + return sort; + }; + + const [query, setQuery] = useState(''); + + const onTableChange = () => { + // we are already updating the state in fetchMoreData. We would need to check in react + // if both methods are needed or we can clean one of them + // For now I just keep it so existing react components don't break + }; + + const getPaginationRouteOptions = useCallback(() => { + if (!pagination || !sorting) { + return {}; + } + + return { + pagination: { + size: pagination.pageSize, + index: pagination.pageIndex, + }, + ...sorting, + queryText: query, + }; + }, [pagination, query, sorting]); + + const getPaginationTableProps = () => { + return { + sorting, + pagination, + onTableChange, + fetchMoreData: ({ + page, + sort, + queryText, + }: { + page: Page; + sort: Sorting; + queryText: string; + }) => { + setPagination({ + ...pagination, + ...{ + initialPageSize: page.size, + pageSize: page.size, + initialPageIndex: page.index, + pageIndex: page.index, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }, + }); + setSorting(cleanSortingData(sort)); + setQuery(queryText); + + setLocalStorageData(storage, { + page, + sort, + }); + }, + }; + }; + + return { + getPaginationRouteOptions, + getPaginationTableProps, + updateTotalItemCount, + }; +} diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index 8cd5bc3088acc..85dc5286efa42 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -20,7 +20,9 @@ import { createPreserveQueryHistory } from './preserve_query_history'; import { RouteInit } from './route_init'; import { NoDataPage } from './pages/no_data'; import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview'; -import { CODE_PATH_ELASTICSEARCH } from '../../common/constants'; +import { BeatsOverviewPage } from './pages/beats/overview'; +import { CODE_PATH_ELASTICSEARCH, CODE_PATH_BEATS } from '../../common/constants'; +import { ElasticsearchNodesPage } from './pages/elasticsearch/nodes_page'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; @@ -77,12 +79,28 @@ const MonitoringApp: React.FC<{ /> {/* ElasticSearch Views */} + + + + {/* Beats Views */} + + = ({ cluster, ...props }) => { + const tabs: TabMenuItem[] = [ + { + id: 'overview', + label: i18n.translate('xpack.monitoring.beatsNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: '/beats', + }, + { + id: 'instances', + label: i18n.translate('xpack.monitoring.beatsNavigation.instancesLinkText', { + defaultMessage: 'Instances', + }), + route: '/beats/beats', + }, + ]; + + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx new file mode 100644 index 0000000000000..3efad7b82549c --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { BeatsTemplate } from './beats_template'; +import { GlobalStateContext } from '../../global_state_context'; +import { useCharts } from '../../hooks/use_charts'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +// @ts-ignore +import { BeatsOverview } from '../../../components/beats/overview'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; + +export const BeatsOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + const [data, setData] = useState(null); + + const title = i18n.translate('xpack.monitoring.beats.overview.routeTitle', { + defaultMessage: 'Beats - Overview', + }); + + const pageTitle = i18n.translate('xpack.monitoring.beats.overview.pageTitle', { + defaultMessage: 'Beats overview', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inBeats: true, + }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/beats`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + const renderOverview = (overviewData: any) => { + if (overviewData === null) { + return null; + } + return ; + }; + + return ( + +
    {renderOverview(data)}
    +
    + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx new file mode 100644 index 0000000000000..652fe83231441 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ElasticsearchTemplate } from './elasticsearch_template'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ExternalConfigContext } from '../../external_config_context'; +import { ElasticsearchNodes } from '../../../components/elasticsearch'; +import { ComponentProps } from '../../route_init'; +import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { useTable } from '../../hooks/use_table'; + +interface SetupModeProps { + setupMode: any; + flyoutComponent: any; + bottomBarComponent: any; +} + +export const ElasticsearchNodesPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { showCgroupMetricsElasticsearch } = useContext(ExternalConfigContext); + const { services } = useKibana<{ data: any }>(); + const { getPaginationRouteOptions, updateTotalItemCount, getPaginationTableProps } = + useTable('elasticsearch.nodes'); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState({} as any); + + const title = i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { + defaultMessage: 'Elasticsearch - Nodes', + }); + + const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.nodes.pageTitle', { + defaultMessage: 'Elasticsearch nodes', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`; + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ...getPaginationRouteOptions(), + }), + }); + + setData(response); + updateTotalItemCount(response.totalNodeCount); + }, [ + ccs, + clusterUuid, + services.data?.query.timefilter.timefilter, + services.http, + getPaginationRouteOptions, + updateTotalItemCount, + ]); + + return ( + +
    + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> +
    +
    + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts index 4460b8432134b..615e79a0bf154 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts @@ -6,3 +6,4 @@ */ export const ElasticsearchOverview: FunctionComponent; +export const ElasticsearchNodes: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/table/index.d.ts b/x-pack/plugins/monitoring/public/components/table/index.d.ts new file mode 100644 index 0000000000000..6b54b3d97e5f1 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/table/index.d.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const euiTableStorageGetter: (string) => any; +export const euiTableStorageSetter: (string) => any; diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 45fe0258dd142..07299f2e6ff1c 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -15,6 +15,7 @@ "home", "lens", "licensing", + "spaces", "usageCollection" ], "requiredPlugins": [ diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 4e912ee4535b8..dc935f3f77787 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../cases/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../rule_registry/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, { "path": "../timelines/tsconfig.json"}, { "path": "../translations/tsconfig.json" } ] diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index 79ff9a6812137..23cc8a302dbef 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -93,11 +93,11 @@ export class ContentStream extends Duplex { this.parameters = { encoding }; } - private async decode(content: string) { + private decode(content: string) { return Buffer.from(content, this.parameters.encoding === 'base64' ? 'base64' : undefined); } - private async encode(buffer: Buffer) { + private encode(buffer: Buffer) { return buffer.toString(this.parameters.encoding === 'base64' ? 'base64' : undefined); } @@ -188,7 +188,7 @@ export class ContentStream extends Duplex { return; } - const buffer = await this.decode(content); + const buffer = this.decode(content); this.push(buffer); this.chunksRead++; @@ -252,7 +252,7 @@ export class ContentStream extends Duplex { private async flush(size = this.buffer.byteLength) { const chunk = this.buffer.slice(0, size); - const content = await this.encode(chunk); + const content = this.encode(chunk); if (!this.chunksWritten) { await this.removeChunks(); @@ -269,32 +269,29 @@ export class ContentStream extends Duplex { this.buffer = this.buffer.slice(size); } - async _write(chunk: Buffer | string, encoding: BufferEncoding, callback: Callback) { + private async flushAllFullChunks() { + const maxChunkSize = await this.getMaxChunkSize(); + + while (this.buffer.byteLength >= maxChunkSize) { + await this.flush(maxChunkSize); + } + } + + _write(chunk: Buffer | string, encoding: BufferEncoding, callback: Callback) { this.buffer = Buffer.concat([ this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), ]); - try { - const maxChunkSize = await this.getMaxChunkSize(); - - while (this.buffer.byteLength >= maxChunkSize) { - await this.flush(maxChunkSize); - } - - callback(); - } catch (error) { - callback(error); - } + this.flushAllFullChunks() + .then(() => callback()) + .catch(callback); } - async _final(callback: Callback) { - try { - await this.flush(); - callback(); - } catch (error) { - callback(error); - } + _final(callback: Callback) { + this.flush() + .then(() => callback()) + .catch(callback); } getSeqNo(): number | undefined { diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 12debe5c85d5e..2017ae0be59c7 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -37,6 +37,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -73,6 +96,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -118,6 +164,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -154,6 +223,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -190,6 +282,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -230,6 +345,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -266,6 +404,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -305,6 +466,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -341,6 +525,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -377,10 +584,56 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, }, + "output_size": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "printable_pdf": Object { "app": Object { "canvas workpad": Object { @@ -413,6 +666,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -449,6 +725,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -973,6 +1272,29 @@ Object { }, }, }, + "output_size": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "printable_pdf": Object { "app": Object { "canvas workpad": Object { @@ -1005,6 +1327,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -1041,6 +1386,29 @@ Object { "type": "long", }, }, + "sizes": Object { + "1.0": Object { + "type": "long", + }, + "25.0": Object { + "type": "long", + }, + "5.0": Object { + "type": "long", + }, + "50.0": Object { + "type": "long", + }, + "75.0": Object { + "type": "long", + }, + "95.0": Object { + "type": "long", + }, + "99.0": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -1620,6 +1988,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 4, }, "csv_searchsource": Object { @@ -1636,6 +2005,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 5, }, "csv_searchsource_immediate": Object { @@ -1703,6 +2073,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 4, }, "csv_searchsource": Object { @@ -1719,6 +2090,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 5, }, "csv_searchsource_immediate": Object { @@ -1737,6 +2109,7 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -1784,6 +2157,7 @@ Object { }, }, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2001,6 +2375,7 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2039,6 +2414,7 @@ Object { }, "statuses": Object {}, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { "canvas workpad": 0, @@ -2079,7 +2455,7 @@ Object { } `; -exports[`data modeling with normal looking usage data 1`] = ` +exports[`data modeling with sparse data 1`] = ` Object { "PNG": Object { "app": Object { @@ -2095,7 +2471,8 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 3, + "output_size": undefined, + "total": 1, }, "PNGV2": Object { "app": Object { @@ -2113,7 +2490,7 @@ Object { }, "total": 0, }, - "_all": 12, + "_all": 4, "available": true, "browser_type": undefined, "csv": Object { @@ -2124,13 +2501,14 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 0, + "deprecated": 1, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 1, }, "csv_searchsource": Object { "app": Object { @@ -2180,6 +2558,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "PNGV2": Object { @@ -2198,7 +2577,7 @@ Object { }, "total": 0, }, - "_all": 1, + "_all": 4, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2207,13 +2586,14 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 0, + "deprecated": 1, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 1, }, "csv_searchsource": Object { "app": Object { @@ -2247,10 +2627,11 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 0, - "dashboard": 0, + "canvas workpad": 1, + "dashboard": 1, "search": 0, "visualization": 0, }, @@ -2258,10 +2639,11 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 0, + "preserve_layout": 2, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 2, }, "printable_pdf_v2": Object { "app": Object { @@ -2280,33 +2662,39 @@ Object { "total": 0, }, "status": Object { - "completed": 0, - "completed_with_warnings": 1, + "completed": 4, "failed": 0, }, "statuses": Object { - "completed_with_warnings": Object { + "completed": Object { "PNG": Object { "dashboard": 1, }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, }, }, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 6, - "dashboard": 0, + "canvas workpad": 1, + "dashboard": 1, "search": 0, - "visualization": 3, + "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 9, + "preserve_layout": 2, "print": 0, }, - "total": 9, + "output_size": undefined, + "total": 2, }, "printable_pdf_v2": Object { "app": Object { @@ -2325,27 +2713,17 @@ Object { "total": 0, }, "status": Object { - "completed": 10, - "completed_with_warnings": 1, - "failed": 1, + "completed": 4, + "failed": 0, }, "statuses": Object { "completed": Object { - "PNG": Object { - "visualization": 1, - }, - "printable_pdf": Object { - "canvas workpad": 6, - "visualization": 3, - }, - }, - "completed_with_warnings": Object { "PNG": Object { "dashboard": 1, }, - }, - "failed": Object { - "PNG": Object { + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, "dashboard": 1, }, }, @@ -2353,7 +2731,7 @@ Object { } `; -exports[`data modeling with sparse data 1`] = ` +exports[`data modeling with usage data from the reporting/archived_reports es archive 1`] = ` Object { "PNG": Object { "app": Object { @@ -2369,6 +2747,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "PNGV2": Object { @@ -2387,7 +2766,7 @@ Object { }, "total": 0, }, - "_all": 4, + "_all": 11, "available": true, "browser_type": undefined, "csv": Object { @@ -2404,6 +2783,7 @@ Object { "preserve_layout": 0, "print": 0, }, + "output_size": undefined, "total": 1, }, "csv_searchsource": Object { @@ -2420,7 +2800,8 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 0, + "output_size": undefined, + "total": 3, }, "csv_searchsource_immediate": Object { "app": Object { @@ -2454,7 +2835,7 @@ Object { "preserve_layout": 0, "print": 0, }, - "total": 1, + "total": 0, }, "PNGV2": Object { "app": Object { @@ -2472,7 +2853,7 @@ Object { }, "total": 0, }, - "_all": 4, + "_all": 0, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2481,13 +2862,13 @@ Object { "visualization": 0, }, "available": true, - "deprecated": 1, + "deprecated": 0, "layout": Object { "canvas": 0, "preserve_layout": 0, "print": 0, }, - "total": 1, + "total": 0, }, "csv_searchsource": Object { "app": Object { @@ -2521,10 +2902,11 @@ Object { }, "total": 0, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 1, - "dashboard": 1, + "canvas workpad": 0, + "dashboard": 0, "search": 0, "visualization": 0, }, @@ -2532,10 +2914,10 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 2, + "preserve_layout": 0, "print": 0, }, - "total": 2, + "total": 0, }, "printable_pdf_v2": Object { "app": Object { @@ -2554,26 +2936,16 @@ Object { "total": 0, }, "status": Object { - "completed": 4, + "completed": 0, "failed": 0, }, - "statuses": Object { - "completed": Object { - "PNG": Object { - "dashboard": 1, - }, - "csv": Object {}, - "printable_pdf": Object { - "canvas workpad": 1, - "dashboard": 1, - }, - }, - }, + "statuses": Object {}, }, + "output_size": undefined, "printable_pdf": Object { "app": Object { - "canvas workpad": 1, - "dashboard": 1, + "canvas workpad": 0, + "dashboard": 6, "search": 0, "visualization": 0, }, @@ -2581,10 +2953,11 @@ Object { "deprecated": 0, "layout": Object { "canvas": 0, - "preserve_layout": 2, - "print": 0, + "preserve_layout": 5, + "print": 1, }, - "total": 2, + "output_size": undefined, + "total": 6, }, "printable_pdf_v2": Object { "app": Object { @@ -2603,17 +2976,38 @@ Object { "total": 0, }, "status": Object { - "completed": 4, - "failed": 0, + "completed": 6, + "completed_with_warnings": 2, + "failed": 2, + "pending": 1, }, "statuses": Object { "completed": Object { + "csv": Object { + "search": 1, + }, + "csv_searchsource": Object { + "search": 3, + }, + "printable_pdf": Object { + "dashboard": 2, + }, + }, + "completed_with_warnings": Object { "PNG": Object { "dashboard": 1, }, - "csv": Object {}, "printable_pdf": Object { - "canvas workpad": 1, + "dashboard": 1, + }, + }, + "failed": Object { + "printable_pdf": Object { + "dashboard": 2, + }, + }, + "pending": Object { + "printable_pdf": Object { "dashboard": 1, }, }, diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts index 782f2e910038e..f74e176e6f21d 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts @@ -11,6 +11,15 @@ import { getExportTypesHandler } from './get_export_type_handler'; import { FeatureAvailabilityMap } from './types'; let featureMap: FeatureAvailabilityMap; +const sizesAggResponse = { + '1.0': 5093470.0, + '5.0': 5093470.0, + '25.0': 5093470.0, + '50.0': 8514532.0, + '75.0': 1.1935594e7, + '95.0': 1.1935594e7, + '99.0': 1.1935594e7, +}; beforeEach(() => { featureMap = { PNG: true, csv: true, csv_searchsource: true, printable_pdf: true }; @@ -67,14 +76,19 @@ test('Model of job status and status-by-pdf-app', () => { test('Model of jobTypes', () => { const result = getExportStats( { - PNG: { available: true, total: 3 }, + PNG: { available: true, total: 3, sizes: sizesAggResponse }, printable_pdf: { available: true, total: 3, + sizes: sizesAggResponse, app: { dashboard: 0, visualization: 0, 'canvas workpad': 3 }, layout: { preserve_layout: 3, print: 0 }, }, - csv_searchsource: { available: true, total: 3 }, + csv_searchsource: { + available: true, + total: 3, + sizes: sizesAggResponse, + }, }, featureMap, exportTypesHandler @@ -95,6 +109,15 @@ test('Model of jobTypes', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -131,6 +154,15 @@ test('Model of jobTypes', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -149,6 +181,15 @@ test('Model of jobTypes', () => { "preserve_layout": 3, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 3, } `); @@ -156,7 +197,14 @@ test('Model of jobTypes', () => { test('PNG counts, provided count of deprecated jobs explicitly', () => { const result = getExportStats( - { PNG: { available: true, total: 15, deprecated: 5 } }, + { + PNG: { + available: true, + total: 15, + deprecated: 5, + sizes: sizesAggResponse, + }, + }, featureMap, exportTypesHandler ); @@ -175,6 +223,15 @@ test('PNG counts, provided count of deprecated jobs explicitly', () => { "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 15, } `); @@ -182,7 +239,14 @@ test('PNG counts, provided count of deprecated jobs explicitly', () => { test('CSV counts, provides all jobs implicitly deprecated due to jobtype', () => { const result = getExportStats( - { csv: { available: true, total: 15, deprecated: 0 } }, + { + csv: { + available: true, + total: 15, + deprecated: 0, + sizes: sizesAggResponse, + }, + }, featureMap, exportTypesHandler ); @@ -201,6 +265,15 @@ test('CSV counts, provides all jobs implicitly deprecated due to jobtype', () => "preserve_layout": 0, "print": 0, }, + "output_size": Object { + "1.0": 5093470, + "25.0": 5093470, + "5.0": 5093470, + "50.0": 8514532, + "75.0": 11935594, + "95.0": 11935594, + "99.0": 11935594, + }, "total": 15, } `); diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.ts index ffdb6cdc290d7..72c09f08017a1 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_stats.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.ts @@ -33,6 +33,7 @@ function getAvailableTotalForFeature( available: isAvailable(featureAvailability, typeKey), total: jobType.total, deprecated, + output_size: jobType.sizes, app: { ...defaultTotalsForFeature.app, ...jobType.app }, layout: { ...defaultTotalsForFeature.layout, ...jobType.layout }, }; @@ -56,6 +57,7 @@ export const getExportStats = ( _all: rangeAll, status: rangeStatus, statuses: rangeStatusByApp, + output_size: outputSize, ...rangeStats } = rangeStatsInput; @@ -84,6 +86,7 @@ export const getExportStats = ( _all: rangeAll || 0, status: { completed: 0, failed: 0, ...rangeStatus }, statuses: rangeStatusByApp, + output_size: outputSize, } as RangeStats; return resultStats; diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 9aba7841162c2..9a452943ff699 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -13,6 +13,7 @@ import type { GetLicense } from './'; import { getExportStats } from './get_export_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import type { + AggregationBuckets, AggregationResultBuckets, AvailableTotal, FeatureAvailabilityMap, @@ -33,6 +34,8 @@ const OBJECT_TYPES_FIELD = 'meta.objectType.keyword'; const STATUS_TYPES_KEY = 'statusTypes'; const STATUS_BY_APP_KEY = 'statusByApp'; const STATUS_TYPES_FIELD = 'status'; +const OUTPUT_SIZES_KEY = 'sizes'; +const OUTPUT_SIZES_FIELD = 'output.size'; const DEFAULT_TERMS_SIZE = 10; const PRINTABLE_PDF_JOBTYPE = 'printable_pdf'; @@ -64,13 +67,14 @@ const getAppStatuses = (buckets: StatusByAppBucket[]) => }, {}); function getAggStats(aggs: AggregationResultBuckets): Partial { - const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY]; + const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY] as AggregationBuckets; const jobTypes = jobBuckets.reduce((accum: JobTypes, bucket) => { - const { key, doc_count: count, isDeprecated } = bucket; + const { key, doc_count: count, isDeprecated, sizes } = bucket; const deprecatedCount = isDeprecated?.doc_count; const total: Omit = { total: count, deprecated: deprecatedCount, + sizes: sizes?.values, }; return { ...accum, [key]: total }; }, {} as JobTypes); @@ -97,7 +101,13 @@ function getAggStats(aggs: AggregationResultBuckets): Partial { statusByApp = getAppStatuses(statusAppBuckets); } - return { _all: all, status: statusTypes, statuses: statusByApp, ...jobTypes }; + return { + _all: all, + status: statusTypes, + statuses: statusByApp, + output_size: get(aggs[OUTPUT_SIZES_KEY], 'values') ?? undefined, + ...jobTypes, + }; } type RangeStatSets = Partial & { @@ -135,7 +145,6 @@ export async function getReportingUsage( exportTypesRegistry: ExportTypesRegistry ): Promise { const reportingIndex = config.get('index'); - const params = { index: `${reportingIndex}-*`, filterPath: 'aggregations.*.buckets', @@ -152,8 +161,14 @@ export async function getReportingUsage( aggs: { [JOB_TYPES_KEY]: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, - aggs: { isDeprecated: { filter: { term: { [OBJECT_TYPE_DEPRECATED_KEY]: true } } } }, + aggs: { + isDeprecated: { filter: { term: { [OBJECT_TYPE_DEPRECATED_KEY]: true } } }, + [OUTPUT_SIZES_KEY]: { + percentiles: { field: OUTPUT_SIZES_FIELD }, + }, + }, }, + [STATUS_TYPES_KEY]: { terms: { field: STATUS_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, [STATUS_BY_APP_KEY]: { terms: { field: 'status', size: DEFAULT_TERMS_SIZE }, @@ -161,19 +176,24 @@ export async function getReportingUsage( jobTypes: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, aggs: { - appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, // NOTE Discover/CSV export is missing the 'meta.objectType' field, so Discover/CSV results are missing for this agg + appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, }, }, }, }, [OBJECT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, - aggs: { pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, + aggs: { + pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, + }, }, [LAYOUT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, aggs: { pdf: { terms: { field: LAYOUT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, }, + [OUTPUT_SIZES_KEY]: { + percentiles: { field: OUTPUT_SIZES_FIELD }, + }, }, }, }, diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index e69e56d6272d5..447085810cfd0 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -18,7 +18,6 @@ import { getReportingUsageCollector, registerReportingUsageCollector, } from './reporting_usage_collector'; -import { SearchResponse } from './types'; const exportTypesRegistry = getExportTypesRegistry(); @@ -190,7 +189,7 @@ describe('data modeling', () => { beforeAll(async () => { mockCore = await createMockReportingCore(createMockConfigSchema()); }); - test('with normal looking usage data', async () => { + test('with usage data from the reporting/archived_reports es archive', async () => { const plugins = getPluginsMock(); const collector = getReportingUsageCollector( mockCore, @@ -202,39 +201,37 @@ describe('data modeling', () => { } ); collectorFetchContext = getMockFetchClients( - getResponseMock( - { + getResponseMock({ aggregations: { ranges: { + meta: {}, buckets: { all: { - doc_count: 12, - jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, - layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, - objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, - statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, - statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, + doc_count: 11, + layoutTypes: { doc_count: 6, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'preserve_layout', doc_count: 5 }, { key: 'print', doc_count: 1 }, ] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 6, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 3, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'search', doc_count: 3 }, ] } }, { key: 'printable_pdf', doc_count: 2, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 2 }, ] } }, { key: 'csv', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'search', doc_count: 1 }, ] } }, ] } }, { key: 'completed_with_warnings', doc_count: 2, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'PNG', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'printable_pdf', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, ] } }, { key: 'failed', doc_count: 2, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 2 }, ] } }, ] } }, { key: 'pending', doc_count: 1, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 1, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 1 }, ] } }, ] } }, ] }, + objectTypes: { doc_count: 6, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'dashboard', doc_count: 6 }, ] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 6 }, { key: 'completed_with_warnings', doc_count: 2 }, { key: 'failed', doc_count: 2 }, { key: 'pending', doc_count: 1 }, ] }, + jobTypes: { meta: {}, doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'printable_pdf', doc_count: 6, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 1713303.0 }, sizeAvg: { value: 957215.0 }, sizeMin: { value: 43226.0 } }, { key: 'csv_searchsource', doc_count: 3, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 7557.0 }, sizeAvg: { value: 3684.6666666666665 }, sizeMin: { value: 204.0 } }, { key: 'PNG', doc_count: 1, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 37748.0 }, sizeAvg: { value: 37748.0 }, sizeMin: { value: 37748.0 } }, { key: 'csv', doc_count: 1, isDeprecated: { meta: {}, doc_count: 0 }, sizeMax: { value: 231.0 }, sizeAvg: { value: 231.0 }, sizeMin: { value: 231.0 } }, ] }, + sizeMax: { value: 1713303.0 }, + sizeMin: { value: 204.0 }, + sizeAvg: { value: 365084.75 }, }, last7Days: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, - }, - lastDay: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + doc_count: 0, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + jobTypes: { meta: {}, doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + sizeMax: { value: null }, + sizeMin: { value: null }, + sizeAvg: { value: null }, }, }, - }, + }, // prettier-ignore }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); @@ -258,121 +255,21 @@ describe('data modeling', () => { buckets: { all: { doc_count: 9, - layoutTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusByApp: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'completed', - doc_count: 9, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'csv_searchsource', - doc_count: 5, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 5 }], - }, - }, - { - key: 'csv', - doc_count: 4, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 4 }], - }, - }, - ], - }, - }, - ], - }, - objectTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'completed', doc_count: 9 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, - { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, - ], - }, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 9, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 5 }] } }, { key: 'csv', doc_count: 4, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 4 }] } }, ] } }, ] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'completed', doc_count: 9 }] }, + jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, ] }, }, last7Days: { doc_count: 9, - layoutTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusByApp: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'completed', - doc_count: 9, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'csv_searchsource', - doc_count: 5, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 5 }], - }, - }, - { - key: 'csv', - doc_count: 4, - appNames: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'search', doc_count: 4 }], - }, - }, - ], - }, - }, - ], - }, - objectTypes: { - doc_count: 0, - pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'completed', doc_count: 9 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, - { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, - ], - }, + layoutTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusByApp: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'completed', doc_count: 9, jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 5 }] } }, { key: 'csv', doc_count: 4, appNames: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'search', doc_count: 4 }] } }, ] } }, ] }, + objectTypes: { doc_count: 0, pdf: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] } }, + statusTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [{ key: 'completed', doc_count: 9 }] }, + jobTypes: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ { key: 'csv_searchsource', doc_count: 5, isDeprecated: { doc_count: 0 } }, { key: 'csv', doc_count: 4, isDeprecated: { doc_count: 4 } }, ] }, }, - }, + }, // prettier-ignore }, }, }) @@ -393,39 +290,30 @@ describe('data modeling', () => { } ); collectorFetchContext = getMockFetchClients( - getResponseMock( - { + getResponseMock({ aggregations: { ranges: { buckets: { all: { doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] } }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] } }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ] } }, ] }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ] }, }, last7Days: { doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] } }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] } }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ] } }, ] }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ] } }, statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ] }, }, - lastDay: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, - }, + }, // prettier-ignore }, }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); @@ -445,9 +333,9 @@ describe('data modeling', () => { collectorFetchContext = getMockFetchClients( getResponseMock({ - aggregations: { - ranges: { - buckets: { + aggregations: { + ranges: { + buckets: { all: { doc_count: 0, jobTypes: { buckets: [] }, @@ -455,6 +343,9 @@ describe('data modeling', () => { objectTypes: { doc_count: 0, pdf: { buckets: [] } }, statusByApp: { buckets: [] }, statusTypes: { buckets: [] }, + sizeMax: { value: null}, + sizeMin: { value: null }, + sizeAvg: { value: null}, }, last7Days: { doc_count: 0, @@ -463,19 +354,15 @@ describe('data modeling', () => { objectTypes: { doc_count: 0, pdf: { buckets: [] } }, statusByApp: { buckets: [] }, statusTypes: { buckets: [] }, + sizeMax: { value: null}, + sizeMin: { value: null }, + sizeAvg: { value: null}, + }, - lastDay: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, + }, // prettier-ignore }, }, - }, - } as SearchResponse) // prettier-ignore + }) ); const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index 02bf65e7c5e4d..9580ddb935dfb 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -14,6 +14,7 @@ import { LayoutCounts, RangeStats, ReportingUsageType, + SizePercentiles, } from './types'; const appCountsSchema: MakeSchemaFrom = { @@ -39,10 +40,21 @@ const byAppCountsSchema: MakeSchemaFrom = { printable_pdf_v2: appCountsSchema, }; +const sizesSchema: MakeSchemaFrom = { + '1.0': { type: 'long' }, + '5.0': { type: 'long' }, + '25.0': { type: 'long' }, + '50.0': { type: 'long' }, + '75.0': { type: 'long' }, + '95.0': { type: 'long' }, + '99.0': { type: 'long' }, +}; + const availableTotalSchema: MakeSchemaFrom = { available: { type: 'boolean' }, total: { type: 'long' }, deprecated: { type: 'long' }, + sizes: sizesSchema, app: appCountsSchema, layout: layoutCountsSchema, }; @@ -74,6 +86,7 @@ const rangeStatsSchema: MakeSchemaFrom = { pending: byAppCountsSchema, processing: byAppCountsSchema, }, + output_size: sizesSchema, }; export const reportingSchema: MakeSchemaFrom = { diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 7bd79de090b37..856d3ad10cb26 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -5,45 +5,57 @@ * 2.0. */ -export interface KeyCountBucket { - key: string; +export interface SizePercentiles { + '1.0': number | null; + '5.0': number | null; + '25.0': number | null; + '50.0': number | null; + '75.0': number | null; + '95.0': number | null; + '99.0': number | null; +} + +interface DocCount { doc_count: number; - isDeprecated?: { - doc_count: number; - }; +} + +interface SizeStats { + sizes?: { values: SizePercentiles }; +} + +export interface KeyCountBucket extends DocCount, SizeStats { + key: string; + isDeprecated?: DocCount; } export interface AggregationBuckets { buckets: KeyCountBucket[]; } -export interface StatusByAppBucket { +export interface StatusByAppBucket extends DocCount { key: string; - doc_count: number; jobTypes: { - buckets: Array<{ - doc_count: number; - key: string; - appNames: AggregationBuckets; - }>; + buckets: Array< + { + key: string; + appNames: AggregationBuckets; + } & DocCount + >; }; } -export interface AggregationResultBuckets { - jobTypes: AggregationBuckets; +export interface AggregationResultBuckets extends DocCount, SizeStats { + jobTypes?: AggregationBuckets; layoutTypes: { - doc_count: number; - pdf: AggregationBuckets; - }; + pdf?: AggregationBuckets; + } & DocCount; objectTypes: { - doc_count: number; - pdf: AggregationBuckets; - }; + pdf?: AggregationBuckets; + } & DocCount; statusTypes: AggregationBuckets; statusByApp: { buckets: StatusByAppBucket[]; }; - doc_count: number; } export interface SearchResponse { @@ -61,6 +73,7 @@ export interface AvailableTotal { available: boolean; total: number; deprecated?: number; + sizes?: SizePercentiles; app?: { search?: number; dashboard?: number; @@ -110,7 +123,8 @@ type StatusByAppCounts = { export type RangeStats = JobTypes & { _all: number; status: StatusCounts; - statuses: StatusByAppCounts; + statuses?: StatusByAppCounts; + output_size?: SizePercentiles; }; export type ReportingUsageType = RangeStats & { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index a93439b29069b..2e2dffa05c9fb 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -64,6 +64,7 @@ export const DEFAULT_INDICATOR_SOURCE_PATH = 'threatintel.indicator'; export const ENRICHMENT_DESTINATION_PATH = 'threat.enrichments'; export const DEFAULT_THREAT_INDEX_KEY = 'securitySolution:defaultThreatIndex'; export const DEFAULT_THREAT_INDEX_VALUE = ['filebeat-*']; +export const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d"'; export enum SecurityPageName { administration = 'administration', diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index f8b3b426580b2..871e50821b58c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -108,6 +108,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; +import { DEFAULT_THREAT_MATCH_QUERY } from '../../../common/constants'; describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { @@ -180,8 +181,8 @@ describe('indicator match', () => { }); describe('custom indicator query input', () => { - it('Has a default set of *:*', () => { - getCustomIndicatorQueryInput().should('have.text', '*:*'); + it(`Has a default set of ${DEFAULT_THREAT_MATCH_QUERY}`, () => { + getCustomIndicatorQueryInput().should('have.text', DEFAULT_THREAT_MATCH_QUERY); }); it('Shows invalidation text if text is removed', () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index c1210bf457b69..b7fb0785736f6 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -473,6 +473,7 @@ export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRul indexField: rule.indicatorMappingField, indicatorIndexField: rule.indicatorIndexField, }); + getCustomIndicatorQueryInput().type('{selectall}{enter}*:*'); getDefineContinueButton().should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/experimental_features_service.ts b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts new file mode 100644 index 0000000000000..813341f175408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExperimentalFeatures } from '../../common/experimental_features'; + +export class ExperimentalFeaturesService { + private static experimentalFeatures?: ExperimentalFeatures; + + public static init({ experimentalFeatures }: { experimentalFeatures: ExperimentalFeatures }) { + this.experimentalFeatures = experimentalFeatures; + } + + public static get(): ExperimentalFeatures { + if (!this.experimentalFeatures) { + this.throwUninitializedError(); + } + + return this.experimentalFeatures; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Experimental features services not initialized - are you trying to import this module from outside of the Security Solution app?' + ); + } +} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 3dfcc62e26a66..785afa49c9791 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -11,7 +11,11 @@ import styled from 'styled-components'; import { isEqual } from 'lodash'; import { IndexPattern } from 'src/plugins/data/public'; -import { DEFAULT_INDEX_KEY, DEFAULT_THREAT_INDEX_KEY } from '../../../../../common/constants'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_THREAT_INDEX_KEY, + DEFAULT_THREAT_MATCH_QUERY, +} from '../../../../../common/constants'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; @@ -72,7 +76,7 @@ const stepDefineDefaultValue: DefineStepRule = { saved_id: undefined, }, threatQueryBar: { - query: { query: '*:*', language: 'kuery' }, + query: { query: DEFAULT_THREAT_MATCH_QUERY, language: 'kuery' }, filters: [], saved_id: undefined, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts index a9c2676396e83..3551d00c50c73 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response_friendly_names.ts @@ -243,6 +243,18 @@ responseMap.set( defaultMessage: 'Events', }) ); +responseMap.set( + 'memory_protection', + i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.memory_protection', { + defaultMessage: 'Memory Threat', + }) +); +responseMap.set( + 'behavior_protection', + i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.behavior_protection', { + defaultMessage: 'Malicious Behavior', + }) +); /** * Maps a server provided value to corresponding i18n'd string. diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx index ed3d9967f318e..5f0c5cca0ad2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/exception_items_summary.tsx @@ -5,9 +5,10 @@ * 2.0. */ +import React, { FC, memo, useCallback } from 'react'; import { EuiBadge, EuiBadgeProps, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; const SUMMARY_KEYS: Readonly> = [ @@ -36,46 +37,76 @@ const SUMMARY_LABELS: Readonly<{ [key in keyof GetExceptionSummaryResponse]: str ), }; +export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` + display: grid; + min-width: 240px; + grid-template-columns: 50% 50%; +`; + +const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ + isSmall: boolean; +}>` + font-size: ${({ isSmall, theme }) => (isSmall ? theme.eui.euiFontSizeXS : 'innherit')}; + font-weight: ${({ isSmall }) => (isSmall ? '1px' : 'innherit')}; +`; + const CSS_BOLD: Readonly = { fontWeight: 'bold' }; interface ExceptionItemsSummaryProps { stats: GetExceptionSummaryResponse | undefined; + isSmall?: boolean; } -export const ExceptionItemsSummary = memo(({ stats }) => { - return ( - - {SUMMARY_KEYS.map((stat) => { - return ( - - - {SUMMARY_LABELS[stat]} - - - ); - })} - - ); -}); +export const ExceptionItemsSummary = memo( + ({ stats, isSmall = false }) => { + const getItem = useCallback( + (stat: keyof GetExceptionSummaryResponse) => ( + + + {SUMMARY_LABELS[stat]} + + + ), + [stats, isSmall] + ); + + return ( + + {SUMMARY_KEYS.map((stat) => getItem(stat))} + + ); + } +); ExceptionItemsSummary.displayName = 'ExceptionItemsSummary'; -const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color'] }> = memo( - ({ children, value, color, ...commonProps }) => { +const SummaryStat: FC<{ value: number; color?: EuiBadgeProps['color']; isSmall?: boolean }> = memo( + ({ children, value, color, isSmall = false, ...commonProps }) => { return ( - - + + {children} {value} - + ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx index 22e1c3a612eb7..41768f4be7d2e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx @@ -46,15 +46,17 @@ export const FleetEventFiltersCard = memo( setStats(summary); } } catch (error) { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError', - { - defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', - values: { error }, - } - ) - ); + if (isMounted.current) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummaryError', + { + defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', + values: { error }, + } + ) + ); + } } }; fetchStats(); @@ -78,12 +80,15 @@ export const FleetEventFiltersCard = memo( path: fleetPackageCustomUrlPath, }, ], - backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), + backButtonUrl: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageCustomUrlPath, + }), }; }, [getAppUrl, pkgkey]); return ( - + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx index 22a7072caea02..aa4b36d548604 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { I18nProvider } from '@kbn/i18n/react'; -import { FleetTrustedAppsCard } from './fleet_trusted_apps_card'; +import { FleetTrustedAppsCardWrapper } from './fleet_trusted_apps_card_wrapper'; import * as reactTestingLibrary from '@testing-library/react'; import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; import { useToasts } from '../../../../../../../common/lib/kibana'; @@ -67,7 +67,9 @@ describe('Fleet trusted apps card', () => { ); // @ts-ignore - const component = reactTestingLibrary.render(, { wrapper: Wrapper }); + const component = reactTestingLibrary.render(, { + wrapper: Wrapper, + }); try { // @ts-ignore await reactTestingLibrary.act(() => promise); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index 4f10eceb6781c..08e8ec39dbaa8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -9,116 +9,87 @@ import React, { memo, useMemo, useState, useEffect, useRef } from 'react'; import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - PackageCustomExtensionComponentProps, - pagePathGetters, -} from '../../../../../../../../../fleet/public'; -import { getTrustedAppsListPath } from '../../../../../../common/routing'; -import { - ListPageRouteState, - GetExceptionSummaryResponse, -} from '../../../../../../../../common/endpoint/types'; -import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; +import { GetExceptionSummaryResponse } from '../../../../../../../../common/endpoint/types'; -import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; import { useKibana, useToasts } from '../../../../../../../common/lib/kibana'; -import { LinkWithIcon } from './link_with_icon'; import { ExceptionItemsSummary } from './exception_items_summary'; import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components'; -export const FleetTrustedAppsCard = memo(({ pkgkey }) => { - const { getAppUrl } = useAppUrl(); - const { - services: { http }, - } = useKibana(); - const toasts = useToasts(); - const [stats, setStats] = useState(); - const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); - const isMounted = useRef(); +interface FleetTrustedAppsCardProps { + customLink: React.ReactNode; + policyId?: string; + cardSize?: 'm' | 'l'; +} - useEffect(() => { - isMounted.current = true; - const fetchStats = async () => { - try { - const response = await trustedAppsApi.getTrustedAppsSummary(); - if (isMounted) { - setStats(response); +export const FleetTrustedAppsCard = memo( + ({ customLink, policyId, cardSize = 'l' }) => { + const { + services: { http }, + } = useKibana(); + const toasts = useToasts(); + const [stats, setStats] = useState(); + const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); + const isMounted = useRef(); + + useEffect(() => { + isMounted.current = true; + const fetchStats = async () => { + try { + const response = await trustedAppsApi.getTrustedAppsSummary({ + kuery: policyId + ? `exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all"` + : undefined, + }); + if (isMounted) { + setStats(response); + } + } catch (error) { + if (isMounted.current) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError', + { + defaultMessage: + 'There was an error trying to fetch trusted apps stats: "{error}"', + values: { error }, + } + ) + ); + } } - } catch (error) { - toasts.addDanger( - i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.trustedAppsSummaryError', - { - defaultMessage: 'There was an error trying to fetch trusted apps stats: "{error}"', - values: { error }, - } - ) - ); - } - }; - fetchStats(); - return () => { - isMounted.current = false; - }; - }, [toasts, trustedAppsApi]); - const trustedAppsListUrlPath = getTrustedAppsListPath(); + }; + fetchStats(); + return () => { + isMounted.current = false; + }; + }, [toasts, trustedAppsApi, policyId]); - const trustedAppRouteState = useMemo(() => { - const fleetPackageCustomUrlPath = `#${ - pagePathGetters.integration_details_custom({ pkgkey })[1] - }`; + const getTitleMessage = () => ( + + ); - return { - backButtonLabel: i18n.translate( - 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', - { defaultMessage: 'Back to Endpoint Integration' } - ), - onBackButtonNavigateTo: [ - INTEGRATIONS_PLUGIN_ID, - { - path: fleetPackageCustomUrlPath, - }, - ], - backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), - }; - }, [getAppUrl, pkgkey]); - return ( - - - - -

    - -

    -
    -
    - - - - - <> - - - - - -
    -
    - ); -}); + return ( + + + + + {cardSize === 'l' ?

    {getTitleMessage()}

    :
    {getTitleMessage()}
    } +
    +
    + + + + + {customLink} + +
    +
    + ); + } +); FleetTrustedAppsCard.displayName = 'FleetTrustedAppsCard'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx new file mode 100644 index 0000000000000..5ac79a5dd5d5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card_wrapper.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + PackageCustomExtensionComponentProps, + pagePathGetters, +} from '../../../../../../../../../fleet/public'; +import { getTrustedAppsListPath } from '../../../../../../common/routing'; +import { ListPageRouteState } from '../../../../../../../../common/endpoint/types'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; + +import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; +import { LinkWithIcon } from './link_with_icon'; +import { FleetTrustedAppsCard } from './fleet_trusted_apps_card'; + +export const FleetTrustedAppsCardWrapper = memo( + ({ pkgkey }) => { + const { getAppUrl } = useAppUrl(); + const trustedAppsListUrlPath = getTrustedAppsListPath(); + + const trustedAppRouteState = useMemo(() => { + const fleetPackageCustomUrlPath = `#${ + pagePathGetters.integration_details_custom({ pkgkey })[1] + }`; + + return { + backButtonLabel: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel', + { defaultMessage: 'Back to Endpoint Integration' } + ), + onBackButtonNavigateTo: [ + INTEGRATIONS_PLUGIN_ID, + { + path: fleetPackageCustomUrlPath, + }, + ], + backButtonUrl: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageCustomUrlPath, + }), + }; + }, [getAppUrl, pkgkey]); + + const customLink = useMemo( + () => ( + + + + ), + [getAppUrl, trustedAppRouteState, trustedAppsListUrlPath] + ); + return ; + } +); + +FleetTrustedAppsCardWrapper.displayName = 'FleetTrustedAppsCardWrapper'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx index 6600fcfddde0c..6aebb130eb896 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/link_with_icon.tsx @@ -13,16 +13,23 @@ import { LinkToAppProps, } from '../../../../../../../common/components/endpoint/link_to_app'; -const LinkLabel = styled.span` +const LinkLabel = styled.span<{ + size?: 'm' | 'l'; +}>` display: inline-block; padding-right: ${(props) => props.theme.eui.paddingSizes.s}; + font-size: ${({ size, theme }) => (size === 'm' ? theme.eui.euiFontSizeXS : 'innherit')}; `; -export const LinkWithIcon: FC = memo(({ children, ...props }) => { +type ComponentProps = LinkToAppProps & { + size?: 'm' | 'l'; +}; + +export const LinkWithIcon: FC = memo(({ children, size = 'l', ...props }) => { return ( - {children} - + {children} + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx index cb128946d8efa..d2d5de5d43a3f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/styled_components.tsx @@ -7,9 +7,12 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` +export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)<{ + cardSize?: 'm' | 'l'; +}>` display: grid; - grid-template-columns: 25% 45% 30%; + grid-template-columns: ${({ cardSize = 'l' }) => + cardSize === 'l' ? '25% 45% 30%' : '30% 35% 35%'}; grid-template-areas: 'title summary link'; `; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx index 094f1131d7034..0748a95f63c9f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/index.tsx @@ -8,14 +8,14 @@ import { EuiSpacer } from '@elastic/eui'; import React, { memo } from 'react'; import { PackageCustomExtensionComponentProps } from '../../../../../../../../fleet/public'; -import { FleetTrustedAppsCard } from './components/fleet_trusted_apps_card'; +import { FleetTrustedAppsCardWrapper } from './components/fleet_trusted_apps_card_wrapper'; import { FleetEventFiltersCard } from './components/fleet_event_filters_card'; export const EndpointPackageCustomExtension = memo( (props) => { return (
    - +
    diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index fe321e6a321c2..0a912598c5722 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -5,19 +5,27 @@ * 2.0. */ -import React, { memo, useEffect, useState } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import React, { memo, useEffect, useState, useMemo } from 'react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { PackagePolicyEditExtensionComponentProps, NewPackagePolicy, + pagePathGetters, } from '../../../../../../../fleet/public'; -import { getPolicyDetailPath } from '../../../../common/routing'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../fleet/common'; +import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; +import { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types'; +import { getPolicyDetailPath, getPolicyTrustedAppsPath } from '../../../../common/routing'; import { PolicyDetailsForm } from '../policy_details_form'; import { AppAction } from '../../../../../common/store/actions'; import { usePolicyDetailsSelector } from '../policy_hooks'; import { policyDetailsForUpdate } from '../../store/policy_details/selectors'; - +import { FleetTrustedAppsCard } from './endpoint_package_custom_extension/components/fleet_trusted_apps_card'; +import { LinkWithIcon } from './endpoint_package_custom_extension/components/link_with_icon'; /** * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy @@ -40,7 +48,12 @@ const WrappedPolicyDetailsForm = memo<{ }>(({ policyId, onChange }) => { const dispatch = useDispatch<(a: AppAction) => void>(); const updatedPolicy = usePolicyDetailsSelector(policyDetailsForUpdate); + const { getAppUrl } = useAppUrl(); const [, setLastUpdatedPolicy] = useState(updatedPolicy); + // TODO: Remove this and related code when removing FF + const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled( + 'trustedAppsByPolicyEnabled' + ); // When the form is initially displayed, trigger the Redux middleware which is based on // the location information stored via the `userChangedUrl` action. @@ -93,9 +106,91 @@ const WrappedPolicyDetailsForm = memo<{ }); }, [onChange, updatedPolicy]); + const policyTrustedAppsPath = useMemo(() => getPolicyTrustedAppsPath(policyId), [policyId]); + const policyTrustedAppRouteState = useMemo(() => { + const fleetPackageIntegrationCustomUrlPath = `#${ + pagePathGetters.integration_policy_edit({ packagePolicyId: policyId })[1] + }`; + + return { + backLink: { + label: i18n.translate( + 'xpack.securitySolution.endpoint.fleetCustomExtension.artifacts.backButtonLabel', + { + defaultMessage: `Back to Fleet integration policy`, + } + ), + navigateTo: [ + INTEGRATIONS_PLUGIN_ID, + { + path: fleetPackageIntegrationCustomUrlPath, + }, + ], + href: getAppUrl({ + appId: INTEGRATIONS_PLUGIN_ID, + path: fleetPackageIntegrationCustomUrlPath, + }), + }, + }; + }, [getAppUrl, policyId]); + + const policyTrustedAppsLink = useMemo( + () => ( + + + + ), + [getAppUrl, policyTrustedAppsPath, policyTrustedAppRouteState] + ); + return (
    - + {isTrustedAppsByPolicyEnabled ? ( + <> +
    + +
    + +
    +
    + + +
    + +
    + +
    + +
    +
    + + +
    + + ) : ( + + )}
    ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx index b657dfc74bdbc..1135a29759315 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/with_security_context.tsx @@ -13,6 +13,8 @@ import { CurrentLicense } from '../../../../../common/components/current_license import { StartPlugins } from '../../../../../types'; import { managementReducer } from '../../../../store/reducer'; import { managementMiddlewareFactory } from '../../../../store/middleware'; +import { appReducer } from '../../../../../common/store/app'; +import { ExperimentalFeaturesService } from '../../../../../common/experimental_features_service'; type ComposeType = typeof compose; declare global { @@ -51,8 +53,15 @@ export const withSecurityContext =

    ({ store = createStore( combineReducers({ management: managementReducer, + app: appReducer, }), - { management: undefined }, + { + management: undefined, + // @ts-ignore ignore this error as we just need the enableExperimental and it's temporary + app: { + enableExperimental: ExperimentalFeaturesService.get(), + }, + }, composeEnhancers(applyMiddleware(...managementMiddlewareFactory(coreStart, depsStart))) ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx index c9d1b3b7882a0..ed6a33166ff59 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -29,14 +29,14 @@ const LOCKED_CARD_RAMSOMWARE_TITLE = i18n.translate( const LOCKED_CARD_MEMORY_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.memory', { - defaultMessage: 'Memory', + defaultMessage: 'Memory Threat', } ); const LOCKED_CARD_BEHAVIOR_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.behavior', { - defaultMessage: 'Behavior', + defaultMessage: 'Malicious Behavior', } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index 9d39ecd05ad8a..c643094e61126 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -28,6 +28,7 @@ import { PutTrustedAppsRequestParams, GetOneTrustedAppRequestParams, GetOneTrustedAppResponse, + GetTrustedAppsSummaryRequest, } from '../../../../../common/endpoint/types/trusted_apps'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; @@ -82,8 +83,10 @@ export class TrustedAppsHttpService implements TrustedAppsService { ); } - async getTrustedAppsSummary() { - return this.http.get(TRUSTED_APPS_SUMMARY_API); + async getTrustedAppsSummary(request: GetTrustedAppsSummaryRequest) { + return this.http.get(TRUSTED_APPS_SUMMARY_API, { + query: request, + }); } getPolicyList(options?: Parameters[1]) { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx index 62bdc446ddb9e..049ab5884b179 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx @@ -171,11 +171,21 @@ export const CreateTrustedAppFlyout = memo( + +

    + {i18n.translate('xpack.securitySolution.trustedApps.detailsSectionTitle', { + defaultMessage: 'Details', + })} +

    +
    + {!isEditMode && ( - -

    {ABOUT_TRUSTED_APPS}

    + <> + +

    {ABOUT_TRUSTED_APPS}

    +
    -
    + )} { const getOsField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => { return renderResult.getByTestId(`${dataTestSub}-osSelectField`) as HTMLButtonElement; }; - const getGlobalSwitchField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => { - return renderResult.getByTestId( - `${dataTestSub}-effectedPolicies-globalSwitch` - ) as HTMLButtonElement; - }; const getDescriptionField = (dataTestSub: string = dataTestSubjForForm): HTMLTextAreaElement => { return renderResult.getByTestId(`${dataTestSub}-descriptionField`) as HTMLTextAreaElement; }; @@ -252,55 +247,50 @@ describe('When using the Trusted App Form', () => { }); describe('the Policy Selection area', () => { - it('should show loader when setting `policies.isLoading` to true', () => { + beforeEach(() => { + const policy = generator.generatePolicyPackagePolicy(); + policy.name = 'test policy A'; + policy.id = '123'; + + formProps.policies.options = [policy]; + }); + + it('should have `global` switch on if effective scope is global and policy options hidden', () => { + render(); + const globalButton = renderResult.getByTestId( + `${dataTestSubjForForm}-effectedPolicies-global` + ) as HTMLButtonElement; + + expect(globalButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); + expect(renderResult.queryByTestId('policy-123')).toBeNull(); + }); + + it('should have policy options visible and specific policies checked if scope is per-policy', () => { + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'policy', + policies: ['123'], + }; + render(); + const perPolicyButton = renderResult.getByTestId( + `${dataTestSubjForForm}-effectedPolicies-perPolicy` + ) as HTMLButtonElement; + + expect(perPolicyButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); + expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual('false'); + expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual('true'); + }); + it('should show loader when setting `policies.isLoading` to true and scope is per-policy', () => { formProps.policies.isLoading = true; + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'policy', + policies: ['123'], + }; render(); expect( renderResult.getByTestId(`${dataTestSubjForForm}-effectedPolicies-policiesSelectable`) .textContent ).toEqual('Loading options'); }); - - describe('and policies exist', () => { - beforeEach(() => { - const policy = generator.generatePolicyPackagePolicy(); - policy.name = 'test policy A'; - policy.id = '123'; - - formProps.policies.options = [policy]; - }); - - it('should display the policies available, but disabled if ', () => { - render(); - expect(renderResult.getByTestId('policy-123')); - }); - - it('should have `global` switch on if effective scope is global and policy options disabled', () => { - render(); - expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('true'); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual( - 'true' - ); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual( - 'false' - ); - }); - - it('should have specific policies checked if scope is per-policy', () => { - (formProps.trustedApp as NewTrustedApp).effectScope = { - type: 'policy', - policies: ['123'], - }; - render(); - expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('false'); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual( - 'false' - ); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual( - 'true' - ); - }); - }); }); describe('the Policy Selection area under feature flag', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index f9b83fd69a75e..5db9a8557fa10 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -14,6 +14,8 @@ import { EuiSuperSelect, EuiSuperSelectOption, EuiTextArea, + EuiText, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiFormProps } from '@elastic/eui/src/components/form/form'; @@ -458,6 +460,41 @@ export const CreateTrustedAppForm = memo( data-test-subj={getTestId('nameTextField')} />
    + + + + + +

    + {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionTitle', { + defaultMessage: 'Conditions', + })} +

    +
    + + +

    + {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionDescription', { + defaultMessage: + 'Select an operating system and add conditions. Availability of conditions may depend on your chosen OS.', + })} +

    +
    + ( data-test-subj={getTestId('conditionsBuilder')} /> - - - - {isTrustedAppsByPolicyEnabled ? ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx index 427d880444d39..4837a816d0ed8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.test.tsx @@ -56,7 +56,7 @@ describe('when using EffectedPolicySelect component', () => { describe('and no policy entries exist', () => { it('should display no options available message', () => { - const { getByTestId } = render(); + const { getByTestId } = render({ isGlobal: false }); expect(getByTestId('test-policiesSelectable').textContent).toEqual('No options available'); }); }); @@ -65,9 +65,15 @@ describe('when using EffectedPolicySelect component', () => { const policyId = 'abc123'; const policyTestSubj = `policy-${policyId}`; - const toggleGlobalSwitch = () => { + const selectGlobalPolicy = () => { act(() => { - fireEvent.click(renderResult.getByTestId('test-globalSwitch')); + fireEvent.click(renderResult.getByTestId('globalPolicy')); + }); + }; + + const selectPerPolicy = () => { + act(() => { + fireEvent.click(renderResult.getByTestId('perPolicy')); }); }; @@ -97,59 +103,41 @@ describe('when using EffectedPolicySelect component', () => { }); it('should display policies', () => { - const { getByTestId } = render(); + const { getByTestId } = render({ isGlobal: false }); expect(getByTestId(policyTestSubj)); }); - it('should disable policy items if global is checked', () => { - const { getByTestId } = render(); - expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('true'); + it('should hide policy items if global is checked', () => { + const { queryByTestId } = render({ isGlobal: true }); + expect(queryByTestId(policyTestSubj)).toBeNull(); }); it('should enable policy items if global is unchecked', async () => { - const { getByTestId } = render(); - toggleGlobalSwitch(); + const { getByTestId } = render({ isGlobal: false }); + selectPerPolicy(); expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('false'); }); it('should call onChange with selection when global is toggled', () => { render(); - toggleGlobalSwitch(); + selectPerPolicy(); expect(handleOnChange.mock.calls[0][0]).toEqual({ isGlobal: false, selected: [], }); - toggleGlobalSwitch(); + selectGlobalPolicy(); expect(handleOnChange.mock.calls[1][0]).toEqual({ isGlobal: true, selected: [], }); }); - it('should not allow clicking on policies when global is true', () => { - render(); - - clickOnPolicy(); - expect(handleOnChange.mock.calls.length).toBe(0); - - // Select a Policy, then switch back to global and try to click the policy again (should be disabled and trigger onChange()) - toggleGlobalSwitch(); - clickOnPolicy(); - toggleGlobalSwitch(); - clickOnPolicy(); - expect(handleOnChange.mock.calls.length).toBe(3); - expect(handleOnChange.mock.calls[2][0]).toEqual({ - isGlobal: true, - selected: [componentProps.options[0]], - }); - }); - - it('should maintain policies selection even if global was checked', () => { + it('should maintain policies selection even if global was checked, and user switched back to per policy', () => { render(); - toggleGlobalSwitch(); + selectPerPolicy(); clickOnPolicy(); expect(handleOnChange.mock.calls[1][0]).toEqual({ isGlobal: false, @@ -157,7 +145,7 @@ describe('when using EffectedPolicySelect component', () => { }); // Toggle isGlobal back to True - toggleGlobalSwitch(); + selectGlobalPolicy(); expect(handleOnChange.mock.calls[2][0]).toEqual({ isGlobal: true, selected: [componentProps.options[0]], diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx index 99db45c0e4b84..bb620ee5e7c01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx @@ -7,12 +7,15 @@ import React, { memo, useCallback, useMemo } from 'react'; import { + EuiButtonGroup, + EuiButtonGroupOptionProps, EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, EuiFormRow, EuiSelectable, EuiSelectableProps, - EuiSwitch, - EuiSwitchProps, + EuiSpacer, EuiText, htmlIdGenerator, } from '@elastic/eui'; @@ -70,6 +73,28 @@ export const EffectedPolicySelect = memo( const getTestId = useTestIdGenerator(dataTestSubj); + const toggleGlobal: EuiButtonGroupOptionProps[] = useMemo( + () => [ + { + id: 'globalPolicy', + label: i18n.translate('xpack.securitySolution.endpoint.trustedAppsByPolicy.global', { + defaultMessage: 'Global', + }), + iconType: isGlobal ? 'checkInCircleFilled' : '', + 'data-test-subj': getTestId('global'), + }, + { + id: 'perPolicy', + label: i18n.translate('xpack.securitySolution.endpoint.trustedAppsByPolicy.perPolicy', { + defaultMessage: 'Per Policy', + }), + iconType: !isGlobal ? 'checkInCircleFilled' : '', + 'data-test-subj': getTestId('perPolicy'), + }, + ], + [getTestId, isGlobal] + ); + const selectableOptions: EffectedPolicyOption[] = useMemo(() => { const isPolicySelected = new Set(selected.map((policy) => policy.id)); @@ -117,10 +142,10 @@ export const EffectedPolicySelect = memo( [isGlobal, onChange] )!; - const handleGlobalSwitchChange: EuiSwitchProps['onChange'] = useCallback( - ({ target: { checked } }) => { + const handleGlobalButtonChange = useCallback( + (selectedId) => { onChange({ - isGlobal: checked, + isGlobal: selectedId === 'globalPolicy', selected, }); }, @@ -138,48 +163,54 @@ export const EffectedPolicySelect = memo( return ( - +

    + +

    + + + + -

    - -

    +

    + {i18n.translate('xpack.securitySolution.trustedApps.assignmentSectionDescription', { + defaultMessage: + 'You can assign this trusted application globally across all policies or assign it to specific policies.', + })} +

    - } - > - -
    - - - {...otherSelectableProps} - options={selectableOptions} - listProps={listProps || DEFAULT_LIST_PROPS} - onChange={handleOnPolicySelectChange} - searchable={true} - data-test-subj={getTestId('policiesSelectable')} - > - {listBuilderCallback} - - + + + + + + + + + {!isGlobal && ( + + + {...otherSelectableProps} + options={selectableOptions} + listProps={listProps || DEFAULT_LIST_PROPS} + onChange={handleOnPolicySelectChange} + searchable={true} + data-test-subj={getTestId('policiesSelectable')} + > + {listBuilderCallback} + + + )}
    ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index c696c4705912e..b9609fb43ada5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -430,8 +430,11 @@ describe('When on the Trusted Apps Page', () => { it('should have list of policies populated', async () => { useIsExperimentalFeatureEnabledMock.mockReturnValue(true); const resetEnv = forceHTMLElementOffsetWidth(); - const { getByTestId } = await renderAndClickAddButton(); - expect(getByTestId('policy-abc123')); + const renderResult = await renderAndClickAddButton(); + act(() => { + fireEvent.click(renderResult.getByTestId('perPolicy')); + }); + expect(renderResult.getByTestId('policy-abc123')); resetEnv(); }); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f8a2de61f5d6f..cd65808f28bce 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -53,6 +53,7 @@ import { import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; import { SecuritySolutionUiConfigType } from './common/types'; +import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; @@ -184,6 +185,7 @@ export class Plugin implements IPlugin) => ({ }, ja3: { terms: { - field: 'tls.client.ja3s', + field: 'tls.client.ja3', }, }, }, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 0f9bc43cdf37d..98c81a6f9c677 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4051,6 +4051,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4093,6 +4118,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4135,6 +4185,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4177,6 +4252,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4219,6 +4319,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4261,6 +4386,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4303,6 +4453,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -4940,6 +5115,31 @@ } } }, + "output_size": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "available": { "type": "boolean" }, @@ -4962,6 +5162,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5004,6 +5229,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5046,6 +5296,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5088,6 +5363,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5130,6 +5430,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5172,6 +5497,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5214,6 +5564,31 @@ "deprecated": { "type": "long" }, + "sizes": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } + }, "app": { "properties": { "search": { @@ -5850,6 +6225,31 @@ } } } + }, + "output_size": { + "properties": { + "1.0": { + "type": "long" + }, + "5.0": { + "type": "long" + }, + "25.0": { + "type": "long" + }, + "50.0": { + "type": "long" + }, + "75.0": { + "type": "long" + }, + "95.0": { + "type": "long" + }, + "99.0": { + "type": "long" + } + } } } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4b5f4b53ae9e6..990a42ffe7a5f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22940,7 +22940,6 @@ "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "クエリ範囲を縮めて結果をさらにフィルタリングしてください", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 結果が多すぎます", "xpack.securitySolution.policiesTab": "ポリシー", - "xpack.securitySolution.policySelect.policySpecificSectionTitle": "特定のエンドポイントポリシーに適用", "xpack.securitySolution.policyStatusText.failure": "失敗", "xpack.securitySolution.policyStatusText.success": "成功", "xpack.securitySolution.policyStatusText.unsupported": "サポートされていない", @@ -23351,8 +23350,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.middleware.editIdMissing": "IDが指定されていません", - "xpack.securitySolution.trustedapps.policySelect.globalSectionTitle": "割り当て", - "xpack.securitySolution.trustedapps.policySelect.globalSwitchTitle": "信頼できるアプリケーションをグローバルに適用", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "フィールド", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "演算子", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "値", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d27c815667b55..e150b474be207 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23318,7 +23318,6 @@ "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "缩减您的查询范围,以更好地筛选结果", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 结果过多", "xpack.securitySolution.policiesTab": "策略", - "xpack.securitySolution.policySelect.policySpecificSectionTitle": "应用到特定终端策略", "xpack.securitySolution.policyStatusText.failure": "失败", "xpack.securitySolution.policyStatusText.success": "成功", "xpack.securitySolution.policyStatusText.unsupported": "不支持", @@ -23732,8 +23731,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.middleware.editIdMissing": "未提供 ID", - "xpack.securitySolution.trustedapps.policySelect.globalSectionTitle": "分配", - "xpack.securitySolution.trustedapps.policySelect.globalSwitchTitle": "全局应用受信任的应用程序", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "字段", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "运算符", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "值", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index ed56ca05538b1..ec86f149e9a43 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -48,6 +48,7 @@ describe('connector validation', () => { secrets: { user: 'user', password: 'pass', + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -70,12 +71,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -86,6 +90,7 @@ describe('connector validation', () => { secrets: { user: null, password: null, + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -108,12 +113,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -141,12 +149,15 @@ describe('connector validation', () => { port: ['Port is required.'], host: ['Host is required.'], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], }, }, }); @@ -156,6 +167,7 @@ describe('connector validation', () => { secrets: { user: 'user', password: null, + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -178,12 +190,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: ['Password is required when username is used.'], + clientSecret: [], }, }, }); @@ -193,6 +208,7 @@ describe('connector validation', () => { secrets: { user: null, password: 'password', + clientSecret: null, }, id: 'test', actionTypeId: '.email', @@ -215,12 +231,15 @@ describe('connector validation', () => { port: [], host: [], service: [], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: ['Username is required when password is used.'], password: [], + clientSecret: [], }, }, }); @@ -253,12 +272,53 @@ describe('connector validation', () => { port: [], host: [], service: ['Service is required.'], + clientId: [], + tenantId: [], }, }, secrets: { errors: { user: [], password: [], + clientSecret: [], + }, + }, + }); + }); + test('connector validation fails when for exchange service selected, but clientId, tenantId and clientSecrets were not defined', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + clientSecret: null, + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + isPreconfigured: false, + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + }, + } as EmailActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + from: [], + port: [], + host: [], + service: [], + clientId: ['Client ID is required.'], + tenantId: ['Tenant ID is required.'], + }, + }, + secrets: { + errors: { + clientSecret: ['Client Secret is required.'], + password: [], + user: [], }, }, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index fe0b18b1b2e61..709101396edf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -14,6 +14,7 @@ import { GenericValidationResult, } from '../../../../types'; import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types'; +import { AdditionalEmailServices } from '../../../../../../actions/common'; const emailServices: EuiSelectOption[] = [ { @@ -106,10 +107,13 @@ export function getActionType(): ActionTypeModel(), host: new Array(), service: new Array(), + clientId: new Array(), + tenantId: new Array(), }; const secretsErrors = { user: new Array(), password: new Array(), + clientSecret: new Array(), }; const validationResult = { @@ -122,17 +126,29 @@ export function getActionType(): ActionTypeModel import('./exchange_form')); export const EmailActionConnectorFields: React.FunctionComponent< ActionConnectorFieldsProps > = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { @@ -61,6 +63,88 @@ export const EmailActionConnectorFields: React.FunctionComponent< password !== undefined && errors.password !== undefined && errors.password.length > 0; const isUserInvalid: boolean = user !== undefined && errors.user !== undefined && errors.user.length > 0; + + const authForm = ( + <> + {getEncryptedFieldNotifyLabel( + !action.id, + 2, + action.isMissingSecrets ?? false, + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel', + { + defaultMessage: + 'Username and password are encrypted. Please reenter values for these fields.', + } + ) + )} + + + + { + editActionSecrets('user', nullableString(e.target.value)); + }} + onBlur={() => { + if (!user) { + editActionSecrets('user', ''); + } + }} + /> + + + + + { + editActionSecrets('password', nullableString(e.target.value)); + }} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + + ); + return ( <> @@ -130,214 +214,149 @@ export const EmailActionConnectorFields: React.FunctionComponent< /> - - - { - editActionConfig('host', e.target.value); - }} - onBlur={() => { - if (!host) { - editActionConfig('host', ''); - } - }} - /> - - - + + {service === AdditionalEmailServices.EXCHANGE ? ( + + ) : ( + <> - { - editActionConfig('port', parseInt(e.target.value, 10)); + editActionConfig('host', e.target.value); }} onBlur={() => { - if (!port) { - editActionConfig('port', 0); + if (!host) { + editActionConfig('host', ''); } }} /> - - - + + { - editActionConfig('secure', e.target.checked); - }} - /> - - + > + { + editActionConfig('port', parseInt(e.target.value, 10)); + }} + onBlur={() => { + if (!port) { + editActionConfig('port', 0); + } + }} + /> + + + + + + { + editActionConfig('secure', e.target.checked); + }} + /> + + + + - - - - - - -

    - -

    -
    - - { - editActionConfig('hasAuth', e.target.checked); - if (!e.target.checked) { - editActionSecrets('user', null); - editActionSecrets('password', null); - } - }} - /> -
    -
    - {hasAuth ? ( - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel', - { - defaultMessage: - 'Username and password are encrypted. Please reenter values for these fields.', - } - ) - )} - + - + +

    + +

    +
    + + - { - editActionSecrets('user', nullableString(e.target.value)); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} - /> -
    -
    - - { + editActionConfig('hasAuth', e.target.checked); + if (!e.target.checked) { + editActionSecrets('user', null); + editActionSecrets('password', null); } - )} - > - { - editActionSecrets('password', nullableString(e.target.value)); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - + }} + />
    + {hasAuth ? authForm : null} - ) : null} + )} ); }; // if the string == null or is empty, return null, else return string -function nullableString(str: string | null | undefined) { +export function nullableString(str: string | null | undefined) { if (str == null || str.trim() === '') return null; return str; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx new file mode 100644 index 0000000000000..2a08c9b19e602 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { EmailActionConnector } from '../types'; +import ExchangeFormFields from './exchange_form'; + +jest.mock('../../../../common/lib/kibana'); + +describe('ExchangeFormFields renders', () => { + test('should display exchange form fields', () => { + const actionConnector = { + secrets: { + clientSecret: 'user', + }, + id: 'test', + actionTypeId: '.email', + name: 'exchange email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + clientId: '123', + tenantId: '1234', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy(); + }); + + test('exchange field defaults to empty when not defined', () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + }, + } as EmailActionConnector; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailClientSecret"]').prop('value')).toEqual(''); + + expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailClientId"]').prop('value')).toEqual(''); + + expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy(); + expect(wrapper.find('input[data-test-subj="emailTenantId"]').prop('value')).toEqual(''); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx new file mode 100644 index 0000000000000..52fa53da19cd8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiFieldText, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiFieldPassword, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IErrorObject } from '../../../../types'; +import { EmailActionConnector } from '../types'; +import { nullableString } from './email_connector'; +import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; + +interface ExchangeFormFieldsProps { + action: EmailActionConnector; + editActionConfig: (property: string, value: unknown) => void; + editActionSecrets: (property: string, value: unknown) => void; + errors: IErrorObject; + readOnly: boolean; +} + +const ExchangeFormFields: React.FunctionComponent = ({ + action, + editActionConfig, + editActionSecrets, + errors, + readOnly, +}) => { + const { tenantId, clientId } = action.config; + const { clientSecret } = action.secrets; + + const isClientIdInvalid: boolean = + clientId !== undefined && errors.clientId !== undefined && errors.clientId.length > 0; + const isTenantIdInvalid: boolean = + tenantId !== undefined && errors.tenantId !== undefined && errors.tenantId.length > 0; + const isClientSecretInvalid: boolean = + clientSecret !== undefined && + errors.clientSecret !== undefined && + errors.clientSecret.length > 0; + + return ( + <> + + + + { + editActionConfig('tenantId', nullableString(e.target.value)); + }} + onBlur={() => { + if (!tenantId) { + editActionConfig('tenantId', ''); + } + }} + /> + + + + + { + editActionConfig('clientId', nullableString(e.target.value)); + }} + onBlur={() => { + if (!clientId) { + editActionConfig('clientId', ''); + } + }} + /> + + + + {getEncryptedFieldNotifyLabel( + !action.id, + 1, + action.isMissingSecrets ?? false, + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel', + { + defaultMessage: 'Client Secret is encrypted. Please reenter value for this field.', + } + ) + )} + + + + { + editActionSecrets('clientSecret', nullableString(e.target.value)); + }} + onBlur={() => { + if (!clientSecret) { + editActionSecrets('clientSecret', ''); + } + }} + /> + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ExchangeFormFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts index df68d0d1237ed..38e16f6046184 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -21,6 +21,27 @@ export const SENDER_NOT_VALID = i18n.translate( } ); +export const CLIENT_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText', + { + defaultMessage: 'Client ID is required.', + } +); + +export const TENANT_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText', + { + defaultMessage: 'Tenant ID is required.', + } +); + +export const CLIENT_SECRET_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText', + { + defaultMessage: 'Client Secret is required.', + } +); + export const PORT_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts index fad71cf5d6385..9e6df1d1a1019 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts @@ -10,6 +10,7 @@ import { HttpSetup } from 'kibana/public'; import { isEmpty } from 'lodash'; import { EmailConfig } from '../types'; import { getServiceConfig } from './api'; +import { AdditionalEmailServices } from '../../../../../../actions/common'; export function useEmailConfig( http: HttpSetup, @@ -39,9 +40,12 @@ export function useEmailConfig( useEffect(() => { (async () => { if (emailService) { + editActionConfig('service', emailService); + if (emailService === AdditionalEmailServices.EXCHANGE) { + return; + } const serviceConfig = await getEmailServiceConfig(emailService); - editActionConfig('service', emailService); editActionConfig('host', serviceConfig?.host ? serviceConfig.host : ''); editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0); editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts index 60e0a0f14b897..abacc5544c712 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/types.ts @@ -79,11 +79,14 @@ export interface EmailConfig { secure?: boolean; hasAuth: boolean; service: string; + clientId?: string; + tenantId?: string; } export interface EmailSecrets { user: string | null; password: string | null; + clientSecret: string | null; } export type EmailActionConnector = UserConfiguredActionConnector; diff --git a/x-pack/test/api_integration/apis/metrics_ui/constants.ts b/x-pack/test/api_integration/apis/metrics_ui/constants.ts index f0ba9b4c368d5..2ca89f2f9ab87 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/constants.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/constants.ts @@ -31,7 +31,8 @@ export const DATES = { 'alert-test-data': { gauge: { min: 1609459200000, // '2022-01-01T00:00:00Z' - max: 1609462800000, // '2021-01-01T01:00:00Z' + max: 1609462800000, // '2021-01-01T01:00:00Z', + midpoint: 1609461000000, // '2021-01-01T00:30:00Z' }, rate: { min: 1609545600000, // '2021-01-02T00:00:00Z' diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts index 28910bbc6b0c8..66c40e2e6e92d 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts @@ -100,7 +100,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -123,7 +123,7 @@ export default function ({ getService }: FtrProviderContext) { it('should alert on the last value when the end date is the same as the last event', async () => { const params = { ...baseParams }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -160,7 +160,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -200,7 +200,7 @@ export default function ({ getService }: FtrProviderContext) { groupBy: ['env'], }; const timeFrame = { end: gauge.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -234,6 +234,53 @@ export default function ({ getService }: FtrProviderContext) { }, ]); }); + + it('should report no data when one of the groups has a data gap', async () => { + const params = { + ...baseParams, + groupBy: ['env'], + }; + const timeFrame = { end: gauge.midpoint }; + const results = await evaluateAlert( + esClient, + params, + configuration, + ['dev', 'prod'], + timeFrame + ); + expect(results).to.eql([ + { + dev: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: null, + timestamp: '2021-01-01T00:25:00.000Z', + shouldFire: [false], + shouldWarn: [false], + isNoData: [true], + isError: false, + }, + prod: { + timeSize: 5, + timeUnit: 'm', + threshold: [1], + comparator: '>=', + aggType: 'sum', + metric: 'value', + currentValue: 0, + timestamp: '2021-01-01T00:25:00.000Z', + shouldFire: [false], + shouldWarn: [false], + isNoData: [false], + isError: false, + }, + }, + ]); + }); }); }); @@ -254,7 +301,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: rate.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -294,7 +341,7 @@ export default function ({ getService }: FtrProviderContext) { ], }; const timeFrame = { end: rate.max }; - const results = await evaluateAlert(esClient, params, configuration, timeFrame); + const results = await evaluateAlert(esClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { diff --git a/x-pack/test/api_integration/apis/security_solution/tls.ts b/x-pack/test/api_integration/apis/security_solution/tls.ts index 2308ad7a0bf34..9fa251ded4e6b 100644 --- a/x-pack/test/api_integration/apis/security_solution/tls.ts +++ b/x-pack/test/api_integration/apis/security_solution/tls.ts @@ -24,7 +24,7 @@ const expectedResult = { _id: '16989191B1A93ECECD5FE9E63EBD4B5C3B606D26', subjects: ['CN=edgecert.googleapis.com,O=Google LLC,L=Mountain View,ST=California,C=US'], issuers: ['CN=GTS CA 1O1,O=Google Trust Services,C=US'], - ja3: [], + ja3: ['bd12d76eb0b6787e6a78a14d2ff96c2b'], notAfter: ['2020-05-06T11:52:15.000Z'], }; @@ -41,7 +41,7 @@ const expectedOverviewDestinationResult = { 'CN=*.cdn.mozilla.net,OU=Cloud Services,O=Mozilla Corporation,L=Mountain View,ST=California,C=US', ], issuers: ['CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US'], - ja3: [], + ja3: ['b20b44b18b853ef29ab773e921b03422'], notAfter: ['2020-12-09T12:00:00.000Z'], }, }, @@ -67,7 +67,7 @@ const expectedOverviewSourceResult = { 'CN=*.cdn.mozilla.net,OU=Cloud Services,O=Mozilla Corporation,L=Mountain View,ST=California,C=US', ], issuers: ['CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US'], - ja3: [], + ja3: ['b20b44b18b853ef29ab773e921b03422'], notAfter: ['2020-12-09T12:00:00.000Z'], }, }, diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 887e6e7894f98..514b54982ee42 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -93,6 +93,8 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) .isDirectory() ); + const casesConfig = ['--xpack.cases.enabled=true']; + return { testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], servers, @@ -115,6 +117,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + ...casesConfig, `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 0021a228ee98b..7367641d71585 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -47,6 +47,7 @@ import { AlertResponse, ConnectorMappings, CasesByAlertId, + CaseResolveResponse, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; @@ -1066,6 +1067,32 @@ export const getCase = async ({ return theCase; }; +export const resolveCase = async ({ + supertest, + caseId, + includeComments = false, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + caseId: string; + includeComments?: boolean; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: theResolvedCase } = await supertest + .get( + `${getSpaceUrlPrefix( + auth?.space + )}${CASES_URL}/${caseId}/resolve?includeComments=${includeComments}` + ) + .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + + return theResolvedCase; +}; + export const findCases = async ({ supertest, query = {}, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts index f21a0ab460424..af8bf464c198d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -11,7 +11,7 @@ import { CASES_URL, SECURITY_SOLUTION_OWNER, } from '../../../../../../plugins/cases/common/constants'; -import { getCase, getCaseSavedObjectsFromES } from '../../../../common/lib/utils'; +import { getCase, getCaseSavedObjectsFromES, resolveCase } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { @@ -207,5 +207,76 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); }); + + describe('7.16.0', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); + }); + + describe('resolve', () => { + it('should return exactMatch outcome', async () => { + const { outcome } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(outcome).to.be('exactMatch'); + }); + + it('should preserve the same case info', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.title).to.be('A case'); + expect(theCase.description).to.be('asdf'); + expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER); + }); + + it('should preserve the same connector', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.connector).to.eql({ + fields: { + issueType: '10002', + parent: null, + priority: null, + }, + id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + name: 'Test Jira', + type: '.jira', + }); + }); + + it('should preserve the same external service', async () => { + const { case: theCase } = await resolveCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.external_service).to.eql({ + connector_id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + connector_name: 'Test Jira', + external_id: '10106', + external_title: 'TPN-99', + external_url: 'https://cases-testing.atlassian.net/browse/TPN-99', + pushed_at: '2021-06-17T18:57:45.524Z', + pushed_by: { + email: null, + full_name: 'j@j.com', + username: '711621466', + }, + }); + }); + }); + }); }); } diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts new file mode 100644 index 0000000000000..27eae507b9a84 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/resolve_case.ts @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + defaultUser, + postCaseReq, + postCaseResp, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + resolveCase, + createComment, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('resolve_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should resolve a case with no comments', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const resolvedCase = await resolveCase({ + supertest, + caseId: postedCase.id, + includeComments: true, + }); + + const data = removeServerGeneratedPropertiesFromCase(resolvedCase.case); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + + it('should resolve a case with comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const resolvedCase = await resolveCase({ + supertest, + caseId: postedCase.id, + includeComments: true, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + resolvedCase.case.comments![0] as AttributesTypeUser + ); + + expect(resolvedCase.case.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should return a 400 when passing the includeSubCaseComments', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/resolve?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(`${CASES_URL}/fake-id/resolve`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + + describe('rbac', () => { + it('should resolve a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const resolvedCase = await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: 'space1' }, + }); + + expect(resolvedCase.case.owner).to.eql('securitySolutionFixture'); + expect(resolvedCase.outcome).to.eql('exactMatch'); + expect(resolvedCase.alias_target_id).to.eql(undefined); + } + }); + + it('should resolve a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + const resolvedCase = await resolveCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: { user: secOnly, space: 'space1' }, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + resolvedCase.case.comments![0] as AttributesTypeUser + ); + + expect(resolvedCase.case.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: getUserInfo(secOnly), + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should not resolve a case when the user does not have access to owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should NOT resolve a case in a space with no permissions', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await resolveCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index fba60634cc3d7..0b933582d84a5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -25,6 +25,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/get_case')); loadTestFile(require.resolve('./cases/patch_cases')); loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/resolve_case')); loadTestFile(require.resolve('./cases/reporters/get_reporters')); loadTestFile(require.resolve('./cases/status/get_status')); loadTestFile(require.resolve('./cases/tags/get_tags')); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index c245b45917497..5e14cc6201ec2 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -28,6 +28,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } + // FLAKY https://github.com/elastic/kibana/issues/113067 describe('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 1d8de9fe9fb6d..ec649935adec2 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const panelActionsTimeRange = getService('dashboardPanelTimeRange'); const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; - describe('Discover Saved Searches', () => { + // FLAKY https://github.com/elastic/kibana/issues/104578 + describe.skip('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); await kibanaServer.importExport.load(ecommerceSOPath); diff --git a/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz index 1c76205f4caa2..92815ba80a3a5 100644 Binary files a/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz and b/x-pack/test/functional/es_archives/infra/alerts_test_data/data.json.gz differ diff --git a/x-pack/test/functional_embedded/tests/iframe_embedded.ts b/x-pack/test/functional_embedded/tests/iframe_embedded.ts index 21bc19424c893..0e2a461dd15f9 100644 --- a/x-pack/test/functional_embedded/tests/iframe_embedded.ts +++ b/x-pack/test/functional_embedded/tests/iframe_embedded.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); - describe('in iframe', () => { + // FLAKY https://github.com/elastic/kibana/issues/70928 + describe.skip('in iframe', () => { it('should open Kibana for logged-in user', async () => { const isChromeHiddenBefore = await PageObjects.common.isChromeHidden(); expect(isChromeHiddenBefore).to.be(true); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 2ce771f7b993f..88ba4c37559c5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -103,7 +103,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('test.always-firing-SelectOption'); } - describe('create alert', function () { + // FLAKY https://github.com/elastic/kibana/issues/112749 + describe.skip('create alert', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts index 728ad056f4e6b..064c6bdc4495e 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/sessions_in_space.ts @@ -23,7 +23,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const searchSessions = getService('searchSessions'); - describe('discover in space', () => { + // FLAKY https://github.com/elastic/kibana/issues/112913 + describe.skip('discover in space', () => { describe('Storing search sessions in space', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/session_in_space'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index a793582cb7295..95299d8a81f5c 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -21,7 +21,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // FLAKY https://github.com/elastic/kibana/issues/100296 + describe.skip('When on the Endpoint Policy Details Page', function () { describe('with an invalid policy id', () => { it('should display an error', async () => { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); @@ -879,6 +880,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); expect(await testSubjects.isSelected('policyWindowsEvent_dns')).to.be(wasSelected); }); + + it('should show trusted apps card and link should go back to policy', async () => { + await testSubjects.existOrFail('fleetTrustedAppsCard'); + await (await testSubjects.find('linkToTrustedApps')).click(); + await testSubjects.existOrFail('policyDetailsPage'); + await (await testSubjects.find('policyDetailsBackLink')).click(); + await testSubjects.existOrFail('endpointIntegrationPolicyForm'); + }); }); }); } diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index b00df7732ea4f..2bfb231887ac2 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -44,6 +44,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // always install Endpoint package by default when Fleet sets up `--xpack.fleet.packages.0.name=endpoint`, `--xpack.fleet.packages.0.version=latest`, + // TODO: Remove feature flags once we're good to go + '--xpack.securitySolution.enableExperimental=["trustedAppsByPolicyEnabled"]', ], }, layout: { diff --git a/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js b/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts similarity index 83% rename from x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js rename to x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts index 85570fc8b0158..4bcdc75d7fa50 100644 --- a/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.js +++ b/x-pack/test/stack_functional_integration/apps/filebeat/filebeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('check filebeat', function () { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); @@ -17,7 +18,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('filebeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Last_30 days'); await retry.try(async () => { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/filebeat/index.js b/x-pack/test/stack_functional_integration/apps/filebeat/index.ts similarity index 70% rename from x-pack/test/stack_functional_integration/apps/filebeat/index.js rename to x-pack/test/stack_functional_integration/apps/filebeat/index.ts index 478914d395c39..24077f40c9324 100644 --- a/x-pack/test/stack_functional_integration/apps/filebeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/filebeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('filebeat app', function () { loadTestFile(require.resolve('./filebeat')); }); diff --git a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts similarity index 54% rename from x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js rename to x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts index 05f03a115f616..801e651d8b92e 100644 --- a/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.js +++ b/x-pack/test/stack_functional_integration/apps/heartbeat/_heartbeat.ts @@ -6,19 +6,23 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'uptime']); - describe('check heartbeat', function () { - it('Uptime app should show snapshot count greater than zero', async function () { + describe('check heartbeat overview page', function () { + it('Uptime app should show 1 UP monitor', async function () { await PageObjects.common.navigateToApp('uptime', { insertTimestamp: false }); await retry.try(async function () { - const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up); - expect(upCount).to.be.greaterThan(0); + const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up, 10); + expect(upCount).to.eql(1); }); }); + it('Uptime app should show Kibana QA Monitor present', async function () { + await PageObjects.uptime.pageHasExpectedIds(['kibana-qa-monitor']); + }); }); } diff --git a/x-pack/test/stack_functional_integration/apps/heartbeat/index.js b/x-pack/test/stack_functional_integration/apps/heartbeat/index.ts similarity index 72% rename from x-pack/test/stack_functional_integration/apps/heartbeat/index.js rename to x-pack/test/stack_functional_integration/apps/heartbeat/index.ts index 226350a74afc0..85c195a9ceff4 100644 --- a/x-pack/test/stack_functional_integration/apps/heartbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/heartbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('heartbeat app', function () { require('./_heartbeat'); loadTestFile(require.resolve('./_heartbeat')); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts similarity index 87% rename from x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts index 34f7c924f5ddb..79dc213e5485a 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); @@ -27,7 +28,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('metricbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts similarity index 93% rename from x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts index ac911a941c146..d2e9adbfd2683 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.ts @@ -8,11 +8,16 @@ import expect from '@kbn/expect'; import { resolve } from 'path'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; const INTEGRATION_TEST_ROOT = process.env.WORKSPACE || resolve(REPO_ROOT, '../integration-test'); const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat'); -export default function ({ getService, getPageObjects, updateBaselines }) { +export default function ({ + getService, + getPageObjects, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { const screenshot = getService('screenshots'); const browser = getService('browser'); const log = getService('log'); diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/index.js b/x-pack/test/stack_functional_integration/apps/metricbeat/index.ts similarity index 74% rename from x-pack/test/stack_functional_integration/apps/metricbeat/index.js rename to x-pack/test/stack_functional_integration/apps/metricbeat/index.ts index 9ee04df965dcc..c4e0db2797b94 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('metricbeat app', function () { loadTestFile(require.resolve('./_metricbeat')); loadTestFile(require.resolve('./_metricbeat_dashboard')); diff --git a/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js b/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts similarity index 88% rename from x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js rename to x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts index a5d6e6e924667..d0d7e326441a0 100644 --- a/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.js +++ b/x-pack/test/stack_functional_integration/apps/packetbeat/_packetbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); @@ -31,7 +32,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('packetbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/packetbeat/index.js b/x-pack/test/stack_functional_integration/apps/packetbeat/index.ts similarity index 71% rename from x-pack/test/stack_functional_integration/apps/packetbeat/index.js rename to x-pack/test/stack_functional_integration/apps/packetbeat/index.ts index ba0af98d21f6b..70e38b6284fbe 100644 --- a/x-pack/test/stack_functional_integration/apps/packetbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/packetbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('packetbeat app', function () { loadTestFile(require.resolve('./_packetbeat')); }); diff --git a/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js b/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts similarity index 87% rename from x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js rename to x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts index c983a9155ae6a..9ef8b85c0ec09 100644 --- a/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.js +++ b/x-pack/test/stack_functional_integration/apps/winlogbeat/_winlogbeat.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); @@ -26,7 +27,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.selectIndexPattern('winlogbeat-*'); await PageObjects.timePicker.setCommonlyUsedTime('Today'); await retry.try(async function () { - const hitCount = parseInt(await PageObjects.discover.getHitCount()); + const hitCount = parseInt(await PageObjects.discover.getHitCount(), 10); expect(hitCount).to.be.greaterThan(0); }); }); diff --git a/x-pack/test/stack_functional_integration/apps/winlogbeat/index.js b/x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts similarity index 71% rename from x-pack/test/stack_functional_integration/apps/winlogbeat/index.js rename to x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts index bb883ee498181..826a292de5659 100644 --- a/x-pack/test/stack_functional_integration/apps/winlogbeat/index.js +++ b/x-pack/test/stack_functional_integration/apps/winlogbeat/index.ts @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; -export default function ({ loadTestFile }) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('winlogbeat app', function () { loadTestFile(require.resolve('./_winlogbeat')); }); diff --git a/yarn.lock b/yarn.lock index 545bd43ed1084..a6f48f9bd0f5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1886,14 +1886,14 @@ dependencies: "@hapi/hoek" "9.x.x" -"@hapi/boom@9.x.x", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.1.tgz#89e6f0e01637c2a4228da0d113e8157c93677b04" - integrity sha512-VNR8eDbBrOxBgbkddRYIe7+8DZ+vSbV6qlmaN2x7eWjsUjy2VmQgChkOKcVZIeupEZYj+I0dqNg430OhwzagjA== +"@hapi/boom@9.x.x", "@hapi/boom@^9.0.0", "@hapi/boom@^9.1.0", "@hapi/boom@^9.1.4": + version "9.1.4" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6" + integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw== dependencies: "@hapi/hoek" "9.x.x" -"@hapi/bounce@2.x.x": +"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d" integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A== @@ -1906,7 +1906,7 @@ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== -"@hapi/call@8.x.x": +"@hapi/call@^8.0.0": version "8.0.1" resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0" integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g== @@ -1914,7 +1914,7 @@ "@hapi/boom" "9.x.x" "@hapi/hoek" "9.x.x" -"@hapi/catbox-memory@5.x.x": +"@hapi/catbox-memory@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.0.tgz#6c18dad1a80737480d1c33bfbefd5d028deec86d" integrity sha512-ByuxVJPHNaXwLzbBv4GdTr6ccpe1nG+AfYt+8ftDWEJY7EWBWzD+Klhy5oPTDGzU26pNUh1e7fcYI1ILZRxAXQ== @@ -1979,29 +1979,29 @@ "@hapi/validate" "1.x.x" "@hapi/wreck" "17.x.x" -"@hapi/hapi@^20.0.3": - version "20.0.3" - resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.0.3.tgz#e72cad460394e6d2c15f9c57abb5d3332dea27e3" - integrity sha512-aqJVHVjoY3phiZsgsGjDRG15CoUNIs1azScqLZDOCZUSKYGTbzPi+K0QP+RUjUJ0m8L9dRuTZ27c8HKxG3wEhA== +"@hapi/hapi@^20.2.0": + version "20.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.2.0.tgz#bf0eca9cc591e83f3d72d06a998d31be35d044a1" + integrity sha512-yPH/z8KvlSLV8lI4EuId9z595fKKk5n6YA7H9UddWYWsBXMcnCyoFmHtYq0PCV4sNgKLD6QW9e27R9V9Z9aqqw== dependencies: "@hapi/accept" "^5.0.1" "@hapi/ammo" "^5.0.1" - "@hapi/boom" "9.x.x" - "@hapi/bounce" "2.x.x" - "@hapi/call" "8.x.x" + "@hapi/boom" "^9.1.0" + "@hapi/bounce" "^2.0.0" + "@hapi/call" "^8.0.0" "@hapi/catbox" "^11.1.1" - "@hapi/catbox-memory" "5.x.x" + "@hapi/catbox-memory" "^5.0.0" "@hapi/heavy" "^7.0.1" - "@hapi/hoek" "9.x.x" - "@hapi/mimos" "5.x.x" + "@hapi/hoek" "^9.0.4" + "@hapi/mimos" "^6.0.0" "@hapi/podium" "^4.1.1" - "@hapi/shot" "^5.0.1" - "@hapi/somever" "3.x.x" + "@hapi/shot" "^5.0.5" + "@hapi/somever" "^3.0.0" "@hapi/statehood" "^7.0.3" "@hapi/subtext" "^7.0.3" - "@hapi/teamwork" "5.x.x" - "@hapi/topo" "5.x.x" - "@hapi/validate" "^1.1.0" + "@hapi/teamwork" "^5.1.0" + "@hapi/topo" "^5.0.0" + "@hapi/validate" "^1.1.1" "@hapi/heavy@^7.0.1": version "7.0.1" @@ -2012,15 +2012,15 @@ "@hapi/hoek" "9.x.x" "@hapi/validate" "1.x.x" -"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4", "@hapi/hoek@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa" - integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw== +"@hapi/hoek@9.x.x", "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4", "@hapi/hoek@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" + integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== -"@hapi/inert@^6.0.3": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-6.0.3.tgz#57af5d912893fabcb57eb4b956f84f6cd8020fe1" - integrity sha512-Z6Pi0Wsn2pJex5CmBaq+Dky9q40LGzXLUIUFrYpDtReuMkmfy9UuUeYc4064jQ1Xe9uuw7kbwE6Fq6rqKAdjAg== +"@hapi/inert@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@hapi/inert/-/inert-6.0.4.tgz#0544221eabc457110a426818358d006e70ff1f41" + integrity sha512-tpmNqtCCAd+5Ts07bJmMaA79+ZUIf0zSWnQMaWtbcO4nGrO/yXB2AzoslfzFX2JEV9vGeF3FfL8mYw0pHl8VGg== dependencies: "@hapi/ammo" "5.x.x" "@hapi/boom" "9.x.x" @@ -2040,10 +2040,10 @@ "@hapi/cryptiles" "5.x.x" "@hapi/hoek" "9.x.x" -"@hapi/mimos@5.x.x": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-5.0.0.tgz#245c6c98b1cc2c13395755c730321b913de074eb" - integrity sha512-EVS6wJYeE73InTlPWt+2e3Izn319iIvffDreci3qDNT+t3lA5ylJ0/SoTaID8e0TPNUkHUSsgJZXEmLHvoYzrA== +"@hapi/mimos@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484" + integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg== dependencies: "@hapi/hoek" "9.x.x" mime-db "1.x.x" @@ -2074,24 +2074,24 @@ "@hapi/hoek" "9.x.x" "@hapi/nigel" "4.x.x" -"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.1.tgz#106e5849f2cb19b8767cc16007e0107f27c3c791" - integrity sha512-jh7a6+5Z4FUWzx8fgmxjaAa1DTBu+Qfg+NbVdo0f++rE5DgsVidUYrLDp3db65+QjDLleA2MfKQXkpT8ylBDXA== +"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1", "@hapi/podium@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26" + integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g== dependencies: "@hapi/hoek" "9.x.x" "@hapi/teamwork" "5.x.x" "@hapi/validate" "1.x.x" -"@hapi/shot@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.4.tgz#6c978314f21a054c041f4becc50095dd78d3d775" - integrity sha512-PcEz0WJgFDA3xNSMeONgQmothFr7jhbbRRSAKaDh7chN7zOXBlhl13bvKZW6CMb2xVfJUmt34CW3e/oExMgBhQ== +"@hapi/shot@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd" + integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A== dependencies: "@hapi/hoek" "9.x.x" "@hapi/validate" "1.x.x" -"@hapi/somever@3.x.x": +"@hapi/somever@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.0.tgz#f4e9b16a948415b926b4dd898013602b0cb45758" integrity sha512-Upw/kmKotC9iEmK4y047HMYe4LDKsE5NWfjgX41XNKmFvxsQL7OiaCWVhuyyhU0ShDGBfIAnCH8jZr49z/JzZA== @@ -2125,19 +2125,19 @@ "@hapi/pez" "^5.0.1" "@hapi/wreck" "17.x.x" -"@hapi/teamwork@5.x.x": +"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.0.tgz#7801a61fc727f702fd2196ef7625eb4e389f4124" integrity sha512-llqoQTrAJDTXxG3c4Kz/uzhBS1TsmSBa/XG5SPcVXgmffHE1nFtyLIK0hNJHCB3EuBKT84adzd1hZNY9GJLWtg== -"@hapi/topo@5.x.x", "@hapi/topo@^5.0.0": +"@hapi/topo@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw== dependencies: "@hapi/hoek" "^9.0.0" -"@hapi/validate@1.x.x", "@hapi/validate@^1.1.0": +"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad" integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA== @@ -5170,41 +5170,42 @@ resolved "https://registry.yarnpkg.com/@types/hapi__catbox/-/hapi__catbox-10.2.3.tgz#c9279c16d709bf2987491c332e11d18124ae018f" integrity sha512-gs6MKMKXzWpSqeYsPaDIDAxD8jLNg7aFxgAJE6Jnc+ns072Z9fuh39/NF5gSk1KNoGCLnIpeZ0etT9gY9QDCKg== -"@types/hapi__cookie@^10.1.1": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-10.1.1.tgz#4420c7f89ef466aa8c1f4d9975c62e6b5b066b1c" - integrity sha512-sWVS20wvqbYSjpjpfOwsD/gtDBba3mi+Y4Yg2qZMBs0/VAgvhOOmpBXzFf2rE8rrEuR44n7tzmEgPWRw5q7kaw== +"@types/hapi__cookie@^10.1.3": + version "10.1.3" + resolved "https://registry.yarnpkg.com/@types/hapi__cookie/-/hapi__cookie-10.1.3.tgz#b0ab2be28669e083c63253927262c43f24395c2c" + integrity sha512-v/hPXxOVfBdkTa+S4cGec88vZjvEbLaZp8xjg2MtjDhykx1/mLtY4EJHk6fI1cW5WGgFV9pgMjz5mOktjNwILw== dependencies: "@types/hapi__hapi" "*" + joi "^17.3.0" -"@types/hapi__h2o2@^8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@types/hapi__h2o2/-/hapi__h2o2-8.3.2.tgz#43cce95972c3097a2ca3efe6b7054a0c95fbf291" - integrity sha512-l36uuLHTwUQNbNUIkT14Z4WbJl1CIWpBZu7ZCBemGBypiNnbJxN3o0YyQ6QAid3YYa2C7LVDIdyY4MhpX8q9ZA== +"@types/hapi__h2o2@^8.3.3": + version "8.3.3" + resolved "https://registry.yarnpkg.com/@types/hapi__h2o2/-/hapi__h2o2-8.3.3.tgz#f6c5ac480a6fd421025f7d0f78dfa916703511b7" + integrity sha512-+qWZVFVGc5Y0wuNZvVe876VJjUBCJ8eQdXovg4Rg9laHpeERQejluI7aw31xXWfLojTuHz3ThZzC6Orqras05Q== dependencies: "@hapi/boom" "^9.0.0" "@hapi/wreck" "^17.0.0" "@types/hapi__hapi" "*" "@types/node" "*" -"@types/hapi__hapi@*", "@types/hapi__hapi@^20.0.2": - version "20.0.2" - resolved "https://registry.yarnpkg.com/@types/hapi__hapi/-/hapi__hapi-20.0.2.tgz#e7571451f7fb75e87ab3873ec91b92f92cd55fff" - integrity sha512-7FwFoaxSCtHXbHbDdArSeVABFOfMLgVkOvOUtWrqUBzw639B2rq9OHv3kOVDHY0bOao0f6ubMzUxio8WQ9QZfQ== +"@types/hapi__hapi@*", "@types/hapi__hapi@^20.0.9": + version "20.0.9" + resolved "https://registry.yarnpkg.com/@types/hapi__hapi/-/hapi__hapi-20.0.9.tgz#9d570846c96268266a14c970c13aeeaccfc8e172" + integrity sha512-fGpKScknCKZityRXdZgpCLGbm41R1ppFgnKHerfZlqOOlCX/jI129S6ghgBqkqCE8m9A0CIu1h7Ch04lD9KOoA== dependencies: "@hapi/boom" "^9.0.0" "@hapi/iron" "^6.0.0" + "@hapi/podium" "^4.1.3" "@types/hapi__catbox" "*" "@types/hapi__mimos" "*" - "@types/hapi__podium" "*" "@types/hapi__shot" "*" - "@types/joi" "*" "@types/node" "*" + joi "^17.3.0" -"@types/hapi__inert@^5.2.2": - version "5.2.2" - resolved "https://registry.yarnpkg.com/@types/hapi__inert/-/hapi__inert-5.2.2.tgz#6513c487d216ed9377c2c0efceb178fda0928bfa" - integrity sha512-Vp9HS2wi3Qbm1oUlcTvzA2Zd+f3Dwg+tgLqWA6KTCgKbQX4LCPKIvVssbaQAVncmcpH0aPrtkAfftJlS/sMsGg== +"@types/hapi__inert@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/hapi__inert/-/hapi__inert-5.2.3.tgz#f586eb240d5997c9968d1b4e8b37679517045ca1" + integrity sha512-I1mWQrEc7oMqGtofT0rwBgRBCBurz0wNzbq8QZsHWR+aXM0bk1j9GA6zwyGIeO53PNl2C1c2kpXlc084xCV+Tg== dependencies: "@types/hapi__hapi" "*" @@ -5215,11 +5216,6 @@ dependencies: "@types/mime-db" "*" -"@types/hapi__podium@*", "@types/hapi__podium@^3.4.1": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/hapi__podium/-/hapi__podium-3.4.1.tgz#826ffed038979c844410e576b574f8237afd59bc" - integrity sha512-qgMyeXTZhGWvvUnXFavW2Pksf07IV1haBM/Fdq6cFi1lCIXhUHsaTrr2w651q+rhHZf+1dgP1vltJ0/quLxYYw== - "@types/hapi__shot@*": version "4.1.1" resolved "https://registry.yarnpkg.com/@types/hapi__shot/-/hapi__shot-4.1.1.tgz#c760322b90eb77f36a3003a442e8dc69e6ae3922" @@ -5361,11 +5357,6 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" -"@types/joi@*": - version "14.3.4" - resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0" - integrity sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A== - "@types/joi@^17.2.3": version "17.2.3" resolved "https://registry.yarnpkg.com/@types/joi/-/joi-17.2.3.tgz#b7768ed9d84f1ebd393328b9f97c1cf3d2b94798"