diff --git a/.bazeliskversion b/.bazeliskversion index 6a126f402d53d..4dae2985b58cc 100644 --- a/.bazeliskversion +++ b/.bazeliskversion @@ -1 +1 @@ -1.7.5 +1.10.1 diff --git a/.bazelrc b/.bazelrc index 524542a02a4aa..91c4870ebd126 100644 --- a/.bazelrc +++ b/.bazelrc @@ -10,9 +10,6 @@ build --remote_timeout=30 build --remote_header=x-buildbuddy-api-key=3EYk49W2NefOx2n3yMze build --remote_accept_cached=true -# BuildBuddy -## Metadata settings -build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" # Enable this in case you want to share your build info # build --build_metadata=VISIBILITY=PUBLIC build --build_metadata=TEST_GROUPS=//packages diff --git a/.bazelrc.common b/.bazelrc.common index 3de2bceaad3a6..c401a90507982 100644 --- a/.bazelrc.common +++ b/.bazelrc.common @@ -120,8 +120,8 @@ test --incompatible_strict_action_env # collect coverage information from test targets coverage --instrument_test_targets -# Settings for CI -# Bazel flags for CI are in /src/dev/ci_setup/.bazelrc-ci +# Metadata settings +build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" # Load any settings specific to the current user. # .bazelrc.user should appear in .gitignore so that settings are not shared with team members diff --git a/.bazelversion b/.bazelversion index fcdb2e109f68c..fae6e3d04b2ca 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -4.0.0 +4.2.1 diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index b9aa0e0e3727a..9cddade0b7482 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -13,6 +13,7 @@ steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build + timeout_in_minutes: 10 - wait @@ -22,6 +23,7 @@ steps: queue: c2-16 key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' @@ -81,6 +83,7 @@ steps: - command: .buildkite/scripts/steps/es_snapshots/trigger_promote.sh label: Trigger promotion + timeout_in_minutes: 10 depends_on: - default-cigroup - default-cigroup-docker @@ -93,3 +96,4 @@ steps: - command: .buildkite/scripts/lifecycle/post_build.sh label: Post-Build + timeout_in_minutes: 10 diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 0edba11836fcd..3337cfb5dfcdd 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -3,6 +3,7 @@ env: steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build + timeout_in_minutes: 10 - wait @@ -12,6 +13,7 @@ steps: queue: c2-16 key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' @@ -143,21 +145,25 @@ steps: agents: queue: n2-2 key: linting + timeout_in_minutes: 90 - command: .buildkite/scripts/steps/checks.sh label: 'Checks' agents: queue: c2-4 key: checks + timeout_in_minutes: 120 - command: .buildkite/scripts/steps/storybooks/build_and_upload.sh label: 'Build Storybooks' agents: queue: c2-4 key: storybooks + timeout_in_minutes: 60 - wait: ~ continue_on_failure: true - command: .buildkite/scripts/lifecycle/post_build.sh label: Post-Build + timeout_in_minutes: 10 diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index bceb1796479a2..2aba49bfa6460 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -4,21 +4,27 @@ env: steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build + timeout_in_minutes: 10 - wait - command: .buildkite/scripts/steps/on_merge_build_and_metrics.sh label: Default Build and Metrics + env: + BAZEL_CACHE_MODE: read-write agents: queue: c2-8 + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/on_merge_ts_refs_api_docs.sh label: Build TS Refs and Check Public API Docs agents: queue: c2-4 + timeout_in_minutes: 80 - wait: ~ continue_on_failure: true - command: .buildkite/scripts/lifecycle/post_build.sh label: Post-Build + timeout_in_minutes: 10 diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 404bfb273b6f7..1013a841dfd27 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -1,6 +1,7 @@ steps: - command: .buildkite/scripts/lifecycle/pre_build.sh label: Pre-Build + timeout_in_minutes: 10 - wait @@ -10,6 +11,7 @@ steps: queue: c2-16 key: build if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' @@ -17,7 +19,7 @@ steps: agents: queue: ci-group-6 depends_on: build - timeout_in_minutes: 120 + timeout_in_minutes: 150 key: default-cigroup retry: automatic: @@ -141,15 +143,18 @@ steps: agents: queue: n2-2 key: linting + timeout_in_minutes: 90 - command: .buildkite/scripts/steps/checks.sh label: 'Checks' agents: queue: c2-4 key: checks + timeout_in_minutes: 120 - command: .buildkite/scripts/steps/storybooks/build_and_upload.sh label: 'Build Storybooks' agents: queue: c2-4 key: storybooks + timeout_in_minutes: 60 diff --git a/.buildkite/pipelines/pull_request/uptime.yml b/.buildkite/pipelines/pull_request/uptime.yml new file mode 100644 index 0000000000000..60fdea1add04c --- /dev/null +++ b/.buildkite/pipelines/pull_request/uptime.yml @@ -0,0 +1,11 @@ +steps: + - command: .buildkite/scripts/steps/functional/uptime.sh + label: 'Uptime @elastic/synthetics Tests' + agents: + queue: ci-group-6 + depends_on: build + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: '*' + limit: 1 diff --git a/.buildkite/pipelines/update_demo_env.yml b/.buildkite/pipelines/update_demo_env.yml index 1c15b227a2e4a..e2dfdd782fd41 100644 --- a/.buildkite/pipelines/update_demo_env.yml +++ b/.buildkite/pipelines/update_demo_env.yml @@ -1,8 +1,10 @@ steps: - command: .buildkite/scripts/steps/demo_env/es_and_init.sh label: Initialize Environment and Deploy ES + timeout_in_minutes: 10 - command: .buildkite/scripts/steps/demo_env/kibana.sh label: Build and Deploy Kibana agents: queue: c2-8 + timeout_in_minutes: 60 diff --git a/.buildkite/scripts/bootstrap.sh b/.buildkite/scripts/bootstrap.sh index 3c6283a4fe3fd..df38c105d2fd3 100755 --- a/.buildkite/scripts/bootstrap.sh +++ b/.buildkite/scripts/bootstrap.sh @@ -3,6 +3,7 @@ set -euo pipefail source .buildkite/scripts/common/util.sh +source .buildkite/scripts/common/setup_bazel.sh echo "--- yarn install and bootstrap" retry 2 15 yarn kbn bootstrap diff --git a/.buildkite/scripts/common/persist_bazel_cache.sh b/.buildkite/scripts/common/persist_bazel_cache.sh deleted file mode 100755 index 357805c11acec..0000000000000 --- a/.buildkite/scripts/common/persist_bazel_cache.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -source .buildkite/scripts/common/util.sh - -KIBANA_BUILDBUDDY_CI_API_KEY=$(retry 5 5 vault read -field=value secret/kibana-issues/dev/kibana-buildbuddy-ci-api-key) -export KIBANA_BUILDBUDDY_CI_API_KEY - -# overwrites the file checkout .bazelrc file with the one intended for CI env -cp "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$KIBANA_DIR/.bazelrc" - -### -### append auth token to buildbuddy into "$KIBANA_DIR/.bazelrc"; -### -echo "# Appended by .buildkite/scripts/persist_bazel_cache.sh" >> "$KIBANA_DIR/.bazelrc" -echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$KIBANA_DIR/.bazelrc" diff --git a/.buildkite/scripts/common/setup_bazel.sh b/.buildkite/scripts/common/setup_bazel.sh new file mode 100755 index 0000000000000..bff44c7ba8dd3 --- /dev/null +++ b/.buildkite/scripts/common/setup_bazel.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +source .buildkite/scripts/common/util.sh + +KIBANA_BUILDBUDDY_CI_API_KEY=$(retry 5 5 vault read -field=value secret/kibana-issues/dev/kibana-buildbuddy-ci-api-key) +export KIBANA_BUILDBUDDY_CI_API_KEY + +echo "[bazel] writing .bazelrc" +cat < $KIBANA_DIR/.bazelrc + # Generated by .buildkite/scripts/common/setup_bazel.sh + + import %workspace%/.bazelrc.common + + build --build_metadata=ROLE=CI +EOF + +if [[ "${BAZEL_CACHE_MODE:-none}" == read* ]]; then + echo "[bazel] enabling caching" +cat <> $KIBANA_DIR/.bazelrc + build --bes_results_url=https://app.buildbuddy.io/invocation/ + build --bes_backend=grpcs://cloud.buildbuddy.io + build --remote_cache=grpcs://cloud.buildbuddy.io + build --remote_timeout=3600 + build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY +EOF +fi + +if [[ "${BAZEL_CACHE_MODE:-none}" == "read" ]]; then + echo "[bazel] cache set to read-only" +cat <> $KIBANA_DIR/.bazelrc + build --noremote_upload_local_results +EOF +fi + +if [[ "${BAZEL_CACHE_MODE:-none}" != @(read|read-write|none|) ]]; then + echo "invalid value for BAZEL_CACHE_MODE received ($BAZEL_CACHE_MODE), expected one of [read,read-write,none]" + exit 1 +fi diff --git a/.buildkite/scripts/common/util.sh b/.buildkite/scripts/common/util.sh index a884a147577c9..0e5fb1c40eb6f 100755 --- a/.buildkite/scripts/common/util.sh +++ b/.buildkite/scripts/common/util.sh @@ -18,7 +18,7 @@ verify_no_git_changes() { RED='\033[0;31m' C_RESET='\033[0m' # Reset color - GIT_CHANGES="$(git ls-files --modified)" + GIT_CHANGES="$(git ls-files --modified -- . ':!:.bazelrc')" if [ "$GIT_CHANGES" ]; then echo -e "\n${RED}ERROR: '$1' caused changes to the following files:${C_RESET}\n" echo -e "$GIT_CHANGES\n" diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 028c90020a0b8..7b5c944d31c1c 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -73,6 +73,10 @@ const uploadPipeline = (pipelineContent) => { // pipeline.push(getPipeline('.buildkite/pipelines/pull_request/apm_cypress.yml')); // } + if (await doAnyChangesMatch([/^x-pack\/plugins\/uptime/])) { + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/uptime.yml')); + } + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/post_build.yml')); uploadPipeline(pipeline.join('\n')); diff --git a/.buildkite/scripts/steps/functional/common.sh b/.buildkite/scripts/steps/functional/common.sh index b60ed835799e5..bedd22c53c7ec 100755 --- a/.buildkite/scripts/steps/functional/common.sh +++ b/.buildkite/scripts/steps/functional/common.sh @@ -2,6 +2,8 @@ set -euo pipefail +# Note, changes here might also need to be made in other scripts, e.g. uptime.sh + source .buildkite/scripts/common/util.sh .buildkite/scripts/bootstrap.sh diff --git a/.buildkite/scripts/steps/functional/uptime.sh b/.buildkite/scripts/steps/functional/uptime.sh new file mode 100755 index 0000000000000..5a59f4dfa48bd --- /dev/null +++ b/.buildkite/scripts/steps/functional/uptime.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/download_build_artifacts.sh + +export JOB=kibana-uptime-playwright + +echo "--- Uptime @elastic/synthetics Tests" + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "Uptime @elastic/synthetics Tests" \ + node plugins/uptime/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" diff --git a/.buildkite/scripts/steps/on_merge_build_and_metrics.sh b/.buildkite/scripts/steps/on_merge_build_and_metrics.sh index 315ba08f8719b..1f1e492f87bec 100755 --- a/.buildkite/scripts/steps/on_merge_build_and_metrics.sh +++ b/.buildkite/scripts/steps/on_merge_build_and_metrics.sh @@ -2,9 +2,6 @@ set -euo pipefail -# Write Bazel cache for Linux -.buildkite/scripts/common/persist_bazel_cache.sh - .buildkite/scripts/bootstrap.sh .buildkite/scripts/build_kibana.sh .buildkite/scripts/post_build_kibana.sh diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 287b376037abe..d3c44eab2a526 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -28,6 +28,7 @@ check_rules_nodejs_version(minimum_version_string = "3.8.0") node_repositories( node_repositories = { "16.11.1-darwin_amd64": ("node-v16.11.1-darwin-x64.tar.gz", "node-v16.11.1-darwin-x64", "ba54b8ed504bd934d03eb860fefe991419b4209824280d4274f6a911588b5e45"), + "16.11.1-darwin_arm64": ("node-v16.11.1-darwin-arm64.tar.gz", "node-v16.11.1-darwin-arm64", "5e772e478390fab3001b7148a923e4f22fca50170000f18b28475337d3a97248"), "16.11.1-linux_arm64": ("node-v16.11.1-linux-arm64.tar.xz", "node-v16.11.1-linux-arm64", "083fc51f0ea26de9041aaf9821874651a9fd3b20d1cf57071ce6b523a0436f17"), "16.11.1-linux_s390x": ("node-v16.11.1-linux-s390x.tar.xz", "node-v16.11.1-linux-s390x", "855b5c83c2ccb05273d50bb04376335c68d47df57f3187cdebe1f22b972d2825"), "16.11.1-linux_amd64": ("node-v16.11.1-linux-x64.tar.xz", "node-v16.11.1-linux-x64", "493bcc9b660eff983a6de65a0f032eb2717f57207edf74c745bcb86e360310b3"), diff --git a/api_docs/security_solution.json b/api_docs/security_solution.json index b16ae8334f1b0..e159e936e8f42 100644 --- a/api_docs/security_solution.json +++ b/api_docs/security_solution.json @@ -772,17 +772,17 @@ "interfaces": [ { "parentPluginId": "securitySolution", - "id": "def-server.AppRequestContext", + "id": "def-server.SecuritySolutionApiRequestHandlerContext", "type": "Interface", "tags": [], - "label": "AppRequestContext", + "label": "SecuritySolutionApiRequestHandlerContext", "description": [], "path": "x-pack/plugins/security_solution/server/types.ts", "deprecated": false, "children": [ { "parentPluginId": "securitySolution", - "id": "def-server.AppRequestContext.getAppClient", + "id": "def-server.SecuritySolutionApiRequestHandlerContext.getAppClient", "type": "Function", "tags": [], "label": "getAppClient", @@ -804,7 +804,7 @@ }, { "parentPluginId": "securitySolution", - "id": "def-server.AppRequestContext.getSpaceId", + "id": "def-server.SecuritySolutionApiRequestHandlerContext.getSpaceId", "type": "Function", "tags": [], "label": "getSpaceId", @@ -819,7 +819,7 @@ }, { "parentPluginId": "securitySolution", - "id": "def-server.AppRequestContext.getExecutionLogClient", + "id": "def-server.SecuritySolutionApiRequestHandlerContext.getExecutionLogClient", "type": "Function", "tags": [], "label": "getExecutionLogClient", @@ -31438,4 +31438,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/dev/build/lib/config.test.ts b/src/dev/build/lib/config.test.ts index 8bc5eb70c9437..b2afe3337230d 100644 --- a/src/dev/build/lib/config.test.ts +++ b/src/dev/build/lib/config.test.ts @@ -107,6 +107,7 @@ describe('#getTargetPlatforms()', () => { .sort() ).toMatchInlineSnapshot(` Array [ + "darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", @@ -132,7 +133,7 @@ describe('#getNodePlatforms()', () => { .getTargetPlatforms() .map((p) => p.getNodeArch()) .sort() - ).toEqual(['darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64']); + ).toEqual(['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64']); }); it('returns this platform and linux, when targetAllPlatforms = false', async () => { diff --git a/src/dev/build/lib/platform.test.ts b/src/dev/build/lib/platform.test.ts index 79b07cac09cfc..193579d1a35c1 100644 --- a/src/dev/build/lib/platform.test.ts +++ b/src/dev/build/lib/platform.test.ts @@ -31,6 +31,7 @@ describe('isWindows()', () => { expect(new Platform('win32', 'x64', 'foo').isWindows()).toBe(true); expect(new Platform('linux', 'x64', 'foo').isWindows()).toBe(false); expect(new Platform('darwin', 'x64', 'foo').isWindows()).toBe(false); + expect(new Platform('darwin', 'arm64', 'foo').isWindows()).toBe(false); }); }); @@ -39,6 +40,7 @@ describe('isLinux()', () => { expect(new Platform('win32', 'x64', 'foo').isLinux()).toBe(false); expect(new Platform('linux', 'x64', 'foo').isLinux()).toBe(true); expect(new Platform('darwin', 'x64', 'foo').isLinux()).toBe(false); + expect(new Platform('darwin', 'arm64', 'foo').isLinux()).toBe(false); }); }); @@ -47,5 +49,6 @@ describe('isMac()', () => { expect(new Platform('win32', 'x64', 'foo').isMac()).toBe(false); expect(new Platform('linux', 'x64', 'foo').isMac()).toBe(false); expect(new Platform('darwin', 'x64', 'foo').isMac()).toBe(true); + expect(new Platform('darwin', 'arm64', 'foo').isMac()).toBe(true); }); }); diff --git a/src/dev/build/lib/platform.ts b/src/dev/build/lib/platform.ts index 2df7801ffc10e..4c4ec271318d6 100644 --- a/src/dev/build/lib/platform.ts +++ b/src/dev/build/lib/platform.ts @@ -49,5 +49,6 @@ export const ALL_PLATFORMS = [ new Platform('linux', 'x64', 'linux-x86_64'), new Platform('linux', 'arm64', 'linux-aarch64'), new Platform('darwin', 'x64', 'darwin-x86_64'), + new Platform('darwin', 'arm64', 'darwin-aarch64'), new Platform('win32', 'x64', 'windows-x86_64'), ]; diff --git a/src/dev/build/tasks/install_chromium.js b/src/dev/build/tasks/install_chromium.js index 95e0df8984f9d..ad60019ea81a4 100644 --- a/src/dev/build/tasks/install_chromium.js +++ b/src/dev/build/tasks/install_chromium.js @@ -16,7 +16,11 @@ export const InstallChromium = { async run(config, log, build) { for (const platform of config.getNodePlatforms()) { - log.info(`Installing Chromium for ${platform.getName()}-${platform.getArchitecture()}`); + const target = `${platform.getName()}-${platform.getArchitecture()}`; + log.info(`Installing Chromium for ${target}`); + + // revert after https://github.com/elastic/kibana/issues/109949 + if (target === 'darwin-arm64') continue; const { binaryPath$ } = installBrowser( log, diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts index ca43f78a40cfd..31374d2050971 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts @@ -98,6 +98,15 @@ it('downloads node builds for each platform', async () => { "url": "darwin:url", }, ], + Array [ + Object { + "destination": "darwin:downloadPath", + "log": , + "retries": 3, + "sha256": "darwin:sha256", + "url": "darwin:url", + }, + ], Array [ Object { "destination": "win32:downloadPath", diff --git a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts index 37a017ed083d0..9f869b99c18ae 100644 --- a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts @@ -105,6 +105,13 @@ it('runs expected fs operations', async () => { "strip": 1, }, ], + Array [ + /.node_binaries//node-v-darwin-arm64.tar.gz, + /.node_binaries//darwin-arm64, + Object { + "strip": 1, + }, + ], ], } `); diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts index b097deb46f61c..c636db145694c 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts @@ -98,6 +98,7 @@ it('checks shasums for each downloaded node build', async () => { Object { "type": "return", "value": Object { + "darwin:darwin-arm64:downloadName": "valid shasum", "darwin:darwin-x64:downloadName": "valid shasum", "linux:linux-arm64:downloadName": "valid shasum", "linux:linux-x64:downloadName": "valid shasum", @@ -134,6 +135,14 @@ it('checks shasums for each downloaded node build', async () => { "name": "darwin", }, ], + Array [ + , + Platform { + "architecture": "arm64", + "buildName": "darwin-aarch64", + "name": "darwin", + }, + ], Array [ , Platform { @@ -165,6 +174,13 @@ it('checks shasums for each downloaded node build', async () => { "downloadPath": "darwin:darwin-x64:downloadPath", }, }, + Object { + "type": "return", + "value": Object { + "downloadName": "darwin:darwin-arm64:downloadName", + "downloadPath": "darwin:darwin-arm64:downloadPath", + }, + }, Object { "type": "return", "value": Object { @@ -190,6 +206,10 @@ it('checks shasums for each downloaded node build', async () => { "darwin:darwin-x64:downloadPath", "sha256", ], + Array [ + "darwin:darwin-arm64:downloadPath", + "sha256", + ], Array [ "win32:win32-x64:downloadPath", "sha256", @@ -212,6 +232,10 @@ it('checks shasums for each downloaded node build', async () => { "type": "return", "value": "valid shasum", }, + Object { + "type": "return", + "value": "valid shasum", + }, ], } `); diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index bb2b9cc96b677..37cb729053785 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -58,6 +58,10 @@ const packages: Package[] = [ url: 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.16.0/linux-arm64-93.gz', sha256: '7a786e0b75985e5aafdefa9af55cad8e85e69a3326f16d8c63d21d6b5b3bff1b', }, + 'darwin-arm64': { + url: 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.16.0/darwin-arm64-93.gz', + sha256: '28b540cdddf13578f1bd28a03e29ffdc26a7f00ec859c369987b8d51ec6357c8', + }, 'win32-x64': { url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/win32-x64-93.gz', sha256: '37245ceb59a086b5e7e9de8746a3cdf148c383be9ae2580f92baea90d0d39947', diff --git a/src/dev/ci_setup/.bazelrc-ci b/src/dev/ci_setup/.bazelrc-ci index 9aee657f37bcb..a0a0c3de73405 100644 --- a/src/dev/ci_setup/.bazelrc-ci +++ b/src/dev/ci_setup/.bazelrc-ci @@ -1,15 +1,5 @@ -# Used in the on-merge job to persist the Bazel cache to BuildBuddy -# from: .buildkite/scripts/common/persist_bazel_cache.sh +# Generated by .buildkite/scripts/common/setup_bazel.sh import %workspace%/.bazelrc.common -# BuildBuddy settings -build --bes_results_url=https://app.buildbuddy.io/invocation/ -build --bes_backend=grpcs://cloud.buildbuddy.io -build --remote_cache=grpcs://cloud.buildbuddy.io -build --remote_timeout=3600 -# --remote_header=x-buildbuddy-api-key= # appended in CI script - -# Metadata settings build --build_metadata=ROLE=CI -build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index c236be18a8e41..e141bfbef3c89 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -34,4 +34,5 @@ export const UI_SETTINGS = { FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', AUTOCOMPLETE_USE_TIMERANGE: 'autocomplete:useTimeRange', AUTOCOMPLETE_VALUE_SUGGESTION_METHOD: 'autocomplete:valueSuggestionMethod', + DATE_FORMAT: 'dateFormat', } as const; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index e4b67c49689ab..f6e5e25f284ca 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -70,6 +70,8 @@ export function DiscoverGridFlyout({ services, setExpandedDoc, }: Props) { + // Get actual hit with updated highlighted searches + const actualHit = useMemo(() => hits?.find(({ _id }) => _id === hit?._id) || hit, [hit, hits]); const pageCount = useMemo(() => (hits ? hits.length : 0), [hits]); const activePage = useMemo(() => { const id = getDocFingerprintId(hit); @@ -188,7 +190,7 @@ export function DiscoverGridFlyout({ { diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx index e2af88b91b3ff..75ec5a62e9299 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx @@ -45,6 +45,7 @@ export class DocViewerTab extends React.Component { shouldComponentUpdate(nextProps: Props, nextState: State) { return ( nextProps.renderProps.hit._id !== this.props.renderProps.hit._id || + !isEqual(nextProps.renderProps.hit.highlight, this.props.renderProps.hit.highlight) || nextProps.id !== this.props.id || !isEqual(nextProps.renderProps.columns, this.props.renderProps.columns) || nextState.hasError diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/__snapshots__/empty_index_list_prompt.test.tsx.snap b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/__snapshots__/empty_index_list_prompt.test.tsx.snap index a104c36e3a8a0..47cad9c7a8216 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/__snapshots__/empty_index_list_prompt.test.tsx.snap +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/__snapshots__/empty_index_list_prompt.test.tsx.snap @@ -120,10 +120,12 @@ exports[`EmptyIndexListPrompt should render normally 1`] = `
- + diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx index d00f9e2368e21..a550209095898 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx @@ -46,14 +46,14 @@ export const EmptyIndexListPrompt = ({ const createAnywayLink = ( createAnyway()} data-test-subj="createAnyway"> ), @@ -153,8 +153,8 @@ export const EmptyIndexListPrompt = ({
- - + + - + = ({ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index b9d1b3992778f..87aa20c4617c1 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -122,7 +122,7 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` @@ -365,7 +365,7 @@ exports[`FieldEditor should render edit scripted field correctly 1`] = ` @@ -658,7 +658,7 @@ exports[`FieldEditor should show conflict field warning 1`] = ` @@ -994,7 +994,7 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` @@ -1343,7 +1343,7 @@ exports[`FieldEditor should show multiple type field warning with a table contai diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 695d02d0744fb..9509f4fb46e0b 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -506,8 +506,7 @@ export class FieldEditor extends PureComponent } > diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 832d895fcea3d..b7d19807e563e 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -28,9 +28,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const setUpQueriesWithFilters = async () => { // set up a query with filters and a time filter log.debug('set up a query with filters to save'); - const fromTime = 'Sep 20, 2015 @ 08:00:00.000'; - const toTime = 'Sep 21, 2015 @ 08:00:00.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + const from = 'Sep 20, 2015 @ 08:00:00.000'; + const to = 'Sep 21, 2015 @ 08:00:00.000'; + await PageObjects.common.setTime({ from, to }); + await PageObjects.common.navigateToApp('discover'); await filterBar.addFilter('extension.raw', 'is one of', 'jpg'); await queryBar.setQuery('response:200'); }; @@ -54,6 +55,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); await esArchiver.unload('test/functional/fixtures/es_archiver/date_nested'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await PageObjects.common.unsetTime(); }); describe('saved query selection', () => { diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 6cc4fda513ea6..bf2db6c25ad12 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -134,7 +134,7 @@ export class TimePickerPageObject extends FtrService { }); // set from time - await this.retry.waitFor(`endDate is set to ${fromTime}`, async () => { + await this.retry.waitFor(`startDate is set to ${fromTime}`, async () => { await this.testSubjects.click('superDatePickerstartDatePopoverButton'); await this.waitPanelIsGone(panel); panel = await this.getTimePickerPanel(); @@ -150,6 +150,7 @@ export class TimePickerPageObject extends FtrService { }); await this.retry.waitFor('Timepicker popover to close', async () => { + await this.browser.pressKeys(this.browser.keys.ESCAPE); return !(await this.testSubjects.exists('superDatePickerAbsoluteDateInput')); }); diff --git a/x-pack/plugins/apm/dev_docs/linting.md b/x-pack/plugins/apm/dev_docs/linting.md index 7db7053e59061..edf3e813a88e9 100644 --- a/x-pack/plugins/apm/dev_docs/linting.md +++ b/x-pack/plugins/apm/dev_docs/linting.md @@ -17,5 +17,5 @@ yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write ### ESLint ``` -node scripts/eslint.js x-pack/legacy/plugins/apm +node scripts/eslint.js x-pack/plugins/apm ``` diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx index e4a851b890a7c..15883e7905142 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx @@ -7,7 +7,7 @@ import { EuiAccordion, EuiAccordionProps } from '@elastic/eui'; import { isEmpty } from 'lodash'; -import React, { useState } from 'react'; +import React, { Dispatch, SetStateAction, useState } from 'react'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Margins } from '../../../../../shared/charts/Timeline'; import { WaterfallItem } from './waterfall_item'; @@ -22,8 +22,8 @@ interface AccordionWaterfallProps { level: number; duration: IWaterfall['duration']; waterfallItemId?: string; + setMaxLevel: Dispatch>; waterfall: IWaterfall; - onToggleEntryTransaction?: () => void; timelineMargins: Margins; onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void; } @@ -97,12 +97,13 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { duration, waterfall, waterfallItemId, + setMaxLevel, timelineMargins, onClickWaterfallItem, - onToggleEntryTransaction, } = props; const nextLevel = level + 1; + setMaxLevel(nextLevel); const children = waterfall.childrenByParentId[item.id] || []; const errorCount = waterfall.getErrorCount(item.id); @@ -139,9 +140,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { forceState={isOpen ? 'open' : 'closed'} onToggle={() => { setIsOpen((isCurrentOpen) => !isCurrentOpen); - if (onToggleEntryTransaction) { - onToggleEntryTransaction(); - } }} > {children.map((child) => ( diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx index 3932a02c9d974..5b4bf99f7dae6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx @@ -29,13 +29,6 @@ const Container = euiStyled.div` overflow: hidden; `; -const TIMELINE_MARGINS = { - top: 40, - left: 100, - right: 50, - bottom: 0, -}; - const toggleFlyout = ({ history, item, @@ -72,6 +65,16 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) { const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc); const errorMarks = getErrorMarks(waterfall.errorItems); + // Calculate the left margin relative to the deepest level, or 100px, whichever + // is more. + const [maxLevel, setMaxLevel] = useState(0); + const timelineMargins = { + top: 40, + left: Math.max(100, maxLevel * 10), + right: 50, + bottom: 0, + }; + return ( @@ -99,7 +102,7 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) { marks={[...agentMarks, ...errorMarks]} xMax={duration} height={waterfallHeight} - margins={TIMELINE_MARGINS} + margins={timelineMargins} />
@@ -110,16 +113,14 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) { isOpen={isAccordionOpen} item={waterfall.entryWaterfallTransaction} level={0} + setMaxLevel={setMaxLevel} waterfallItemId={waterfallItemId} duration={duration} waterfall={waterfall} - timelineMargins={TIMELINE_MARGINS} + timelineMargins={timelineMargins} onClickWaterfallItem={(item: IWaterfallItem) => toggleFlyout({ history, item }) } - onToggleEntryTransaction={() => - setIsAccordionOpen((isOpen) => !isOpen) - } /> )} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx index 4001a0624a809..caa0cac3acef8 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx @@ -17,6 +17,7 @@ import { } from '../../../../../../../common/elasticsearch_fieldnames'; import { asDuration } from '../../../../../../../common/utils/formatters'; import { Margins } from '../../../../../shared/charts/Timeline'; +import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip'; import { SyncBadge } from './sync_badge'; import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; import { FailureBadge } from './failure_badge'; @@ -67,6 +68,7 @@ const ItemText = euiStyled.span` display: flex; align-items: center; height: ${({ theme }) => theme.eui.euiSizeL}; + max-width: 100%; /* add margin to all direct descendants */ & > * { @@ -160,7 +162,11 @@ function NameLabel({ item }: { item: IWaterfallSpanOrTransaction }) { : ''; name = `${item.doc.span.composite.count}${compositePrefix} ${name}`; } - return {name}; + return ( + + + + ); case 'transaction': return ( diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx deleted file mode 100644 index a03b7b29f9666..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ComponentType } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; -import { WaterfallContainer } from './index'; -import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; -import { - inferredSpans, - simpleTrace, - traceChildStartBeforeParent, - traceWithErrors, - urlParams, -} from './waterfallContainer.stories.data'; - -export default { - title: 'app/TransactionDetails/Waterfall', - component: WaterfallContainer, - decorators: [ - (Story: ComponentType) => ( - - - - - - ), - ], -}; - -export function Example() { - const waterfall = getWaterfall(simpleTrace, '975c8d5bfd1dd20b'); - return ; -} - -export function WithErrors() { - const waterfall = getWaterfall(traceWithErrors, '975c8d5bfd1dd20b'); - return ; -} - -export function ChildStartsBeforeParent() { - const waterfall = getWaterfall( - traceChildStartBeforeParent, - '975c8d5bfd1dd20b' - ); - return ; -} - -export function InferredSpans() { - const waterfall = getWaterfall(inferredSpans, 'f2387d37260d00bd'); - return ; -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts deleted file mode 100644 index 60285c835bbf3..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts +++ /dev/null @@ -1,2269 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Location } from 'history'; -import type { ApmUrlParams } from '../../../../../context/url_params_context/types'; -import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; - -export const location = { - pathname: '/services/opbeans-go/transactions/view', - search: - '?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=service.name%253A%2520%2522opbeans-java%2522%2520or%2520service.name%2520%253A%2520%2522opbeans-go%2522&traceId=513d33fafe99bbe6134749310c9b5322&transactionId=975c8d5bfd1dd20b&transactionName=GET%20%2Fapi%2Forders&transactionType=request', - hash: '', -} as Location; - -type TraceAPIResponse = APIReturnType<'GET /internal/apm/traces/{traceId}'>; - -export const urlParams = { - start: '2020-03-22T15:16:38.742Z', - end: '2020-03-23T15:16:38.742Z', - rangeFrom: 'now-24h', - rangeTo: 'now', - refreshPaused: true, - refreshInterval: 0, - page: 0, - transactionId: '975c8d5bfd1dd20b', - traceId: '513d33fafe99bbe6134749310c9b5322', - transactionName: 'GET /api/orders', - transactionType: 'request', - processorEvent: 'transaction', - serviceName: 'opbeans-go', -} as ApmUrlParams; - -export const simpleTrace = { - traceDocs: [ - { - container: { - id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', - }, - internal: { - sampler: { - value: 46, - }, - }, - source: { - ip: '172.19.0.13', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: '172.19.0.9', - full: 'http://172.19.0.9:3000/api/orders', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.19.0.9:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.19.0.13', - }, - body: { - original: '[REDACTED]', - }, - }, - response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], - 'Content-Type': ['application/json;charset=ISO-8859-1'], - }, - status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', - }, - client: { - ip: '172.19.0.13', - }, - transaction: { - duration: { - us: 18842, - }, - result: 'HTTP 2xx', - name: 'DispatcherServlet#doGet', - id: '49809ad3c26adf74', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, - }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', - name: 'Other', - device: { - name: 'Other', - }, - }, - timestamp: { - us: 1584975868785000, - }, - }, - { - parent: { - id: 'fc107f7b556eb49b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - framework: { - name: 'gin', - version: 'v1.4.0', - }, - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - duration: { - us: 16597, - }, - result: 'HTTP 2xx', - name: 'GET /api/orders', - id: '975c8d5bfd1dd20b', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, - }, - timestamp: { - us: 1584975868787052, - }, - }, - { - parent: { - id: 'daae24d83c269918', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - timestamp: { - us: 1584975868788603, - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - '@timestamp': '2020-03-23T15:04:28.788Z', - service: { - node: { - name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', - }, - transaction: { - result: 'HTTP 2xx', - duration: { - us: 14648, - }, - name: 'GET opbeans.views.orders', - span_count: { - dropped: 0, - started: 1, - }, - id: '6fb0ff7365b87298', - type: 'request', - sampled: true, - }, - }, - { - container: { - id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - parent: { - id: '49809ad3c26adf74', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', - }, - internal: { - sampler: { - value: 44, - }, - }, - destination: { - address: 'opbeans-go', - port: 3000, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - type: 'apm-server', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - connection: { - hash: "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", - }, - transaction: { - id: '49809ad3c26adf74', - }, - timestamp: { - us: 1584975868785273, - }, - span: { - duration: { - us: 17530, - }, - subtype: 'http', - name: 'GET opbeans-go', - destination: { - service: { - resource: 'opbeans-go:3000', - name: 'http://opbeans-go:3000', - type: 'external', - }, - }, - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-go:3000/api/orders', - }, - }, - id: 'fc107f7b556eb49b', - type: 'external', - }, - }, - { - parent: { - id: '975c8d5bfd1dd20b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', - }, - timestamp: { - us: 1584975868787174, - }, - span: { - duration: { - us: 16250, - }, - subtype: 'http', - destination: { - service: { - resource: 'opbeans-python:3000', - name: 'http://opbeans-python:3000', - type: 'external', - }, - }, - name: 'GET opbeans-python:3000', - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-python:3000/api/orders', - }, - }, - id: 'daae24d83c269918', - type: 'external', - }, - }, - { - container: { - id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - parent: { - id: '6fb0ff7365b87298', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.790Z', - service: { - node: { - name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', - }, - transaction: { - id: '6fb0ff7365b87298', - }, - timestamp: { - us: 1584975868790080, - }, - span: { - duration: { - us: 2519, - }, - subtype: 'postgresql', - name: 'SELECT FROM opbeans_order', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - action: 'query', - id: 'c9407abb4d08ead1', - type: 'db', - sync: true, - db: { - statement: - 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', - type: 'sql', - }, - }, - }, - ], - exceedsMax: false, - errorDocs: [], -} as TraceAPIResponse; - -export const traceWithErrors = { - traceDocs: [ - { - container: { - id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', - }, - internal: { - sampler: { - value: 46, - }, - }, - source: { - ip: '172.19.0.13', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: '172.19.0.9', - full: 'http://172.19.0.9:3000/api/orders', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.19.0.9:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.19.0.13', - }, - body: { - original: '[REDACTED]', - }, - }, - response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], - 'Content-Type': ['application/json;charset=ISO-8859-1'], - }, - status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', - }, - client: { - ip: '172.19.0.13', - }, - transaction: { - duration: { - us: 18842, - }, - result: 'HTTP 2xx', - name: 'DispatcherServlet#doGet', - id: '49809ad3c26adf74', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, - }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', - name: 'Other', - device: { - name: 'Other', - }, - }, - timestamp: { - us: 1584975868785000, - }, - }, - { - parent: { - id: 'fc107f7b556eb49b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - framework: { - name: 'gin', - version: 'v1.4.0', - }, - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - duration: { - us: 16597, - }, - result: 'HTTP 2xx', - name: 'GET /api/orders', - id: '975c8d5bfd1dd20b', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, - }, - timestamp: { - us: 1584975868787052, - }, - }, - { - parent: { - id: 'daae24d83c269918', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - timestamp: { - us: 1584975868788603, - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - '@timestamp': '2020-03-23T15:04:28.788Z', - service: { - node: { - name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', - }, - transaction: { - result: 'HTTP 2xx', - duration: { - us: 14648, - }, - name: 'GET opbeans.views.orders', - span_count: { - dropped: 0, - started: 1, - }, - id: '6fb0ff7365b87298', - type: 'request', - sampled: true, - }, - }, - { - container: { - id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - parent: { - id: '49809ad3c26adf74', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', - }, - internal: { - sampler: { - value: 44, - }, - }, - destination: { - address: 'opbeans-go', - port: 3000, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - type: 'apm-server', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - connection: { - hash: "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", - }, - transaction: { - id: '49809ad3c26adf74', - }, - timestamp: { - us: 1584975868785273, - }, - span: { - duration: { - us: 17530, - }, - subtype: 'http', - name: 'GET opbeans-go', - destination: { - service: { - resource: 'opbeans-go:3000', - name: 'http://opbeans-go:3000', - type: 'external', - }, - }, - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-go:3000/api/orders', - }, - }, - id: 'fc107f7b556eb49b', - type: 'external', - }, - }, - { - parent: { - id: '975c8d5bfd1dd20b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', - }, - timestamp: { - us: 1584975868787174, - }, - span: { - duration: { - us: 16250, - }, - subtype: 'http', - destination: { - service: { - resource: 'opbeans-python:3000', - name: 'http://opbeans-python:3000', - type: 'external', - }, - }, - name: 'GET opbeans-python:3000', - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-python:3000/api/orders', - }, - }, - id: 'daae24d83c269918', - type: 'external', - }, - }, - { - container: { - id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - parent: { - id: '6fb0ff7365b87298', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.790Z', - service: { - node: { - name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', - }, - transaction: { - id: '6fb0ff7365b87298', - }, - timestamp: { - us: 1584975868790080, - }, - span: { - duration: { - us: 2519, - }, - subtype: 'postgresql', - name: 'SELECT FROM opbeans_order', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - action: 'query', - id: 'c9407abb4d08ead1', - type: 'db', - sync: true, - db: { - statement: - 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', - type: 'sql', - }, - }, - }, - ], - exceedsMax: false, - errorDocs: [ - { - parent: { - id: '975c8d5bfd1dd20b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - error: { - culprit: 'logrusMiddleware', - log: { - level: 'error', - message: 'GET //api/products (502)', - }, - id: '1f3cb98206b5c54225cb7c8908a658da', - grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', - }, - processor: { - name: 'error', - event: 'error', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T16:04:28.787Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', - sampled: false, - }, - timestamp: { - us: 1584975868787052, - }, - }, - { - parent: { - id: '6fb0ff7365b87298', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - error: { - culprit: 'logrusMiddleware', - log: { - level: 'error', - message: 'GET //api/products (502)', - }, - id: '1f3cb98206b5c54225cb7c8908a658d2', - grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', - }, - processor: { - name: 'error', - event: 'error', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T16:04:28.790Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-python', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '6fb0ff7365b87298', - sampled: false, - }, - timestamp: { - us: 1584975868790000, - }, - }, - ], -} as unknown as TraceAPIResponse; - -export const traceChildStartBeforeParent = { - traceDocs: [ - { - container: { - id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', - }, - internal: { - sampler: { - value: 46, - }, - }, - source: { - ip: '172.19.0.13', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: '172.19.0.9', - full: 'http://172.19.0.9:3000/api/orders', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.19.0.9:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.19.0.13', - }, - body: { - original: '[REDACTED]', - }, - }, - response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], - 'Content-Type': ['application/json;charset=ISO-8859-1'], - }, - status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', - }, - client: { - ip: '172.19.0.13', - }, - transaction: { - duration: { - us: 18842, - }, - result: 'HTTP 2xx', - name: 'DispatcherServlet#doGet', - id: '49809ad3c26adf74', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, - }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', - name: 'Other', - device: { - name: 'Other', - }, - }, - timestamp: { - us: 1584975868785000, - }, - }, - { - parent: { - id: 'fc107f7b556eb49b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - framework: { - name: 'gin', - version: 'v1.4.0', - }, - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - duration: { - us: 16597, - }, - result: 'HTTP 2xx', - name: 'GET /api/orders', - id: '975c8d5bfd1dd20b', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, - }, - timestamp: { - us: 1584975868787052, - }, - }, - { - parent: { - id: 'daae24d83c269918', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - timestamp: { - us: 1584975868780000, - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - '@timestamp': '2020-03-23T15:04:28.788Z', - service: { - node: { - name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', - }, - transaction: { - result: 'HTTP 2xx', - duration: { - us: 1464, - }, - name: 'I started before my parent 😰', - span_count: { - dropped: 0, - started: 1, - }, - id: '6fb0ff7365b87298', - type: 'request', - sampled: true, - }, - }, - { - container: { - id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - parent: { - id: '49809ad3c26adf74', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', - }, - internal: { - sampler: { - value: 44, - }, - }, - destination: { - address: 'opbeans-go', - port: 3000, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - type: 'apm-server', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - connection: { - hash: "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", - }, - transaction: { - id: '49809ad3c26adf74', - }, - timestamp: { - us: 1584975868785273, - }, - span: { - duration: { - us: 17530, - }, - subtype: 'http', - name: 'GET opbeans-go', - destination: { - service: { - resource: 'opbeans-go:3000', - name: 'http://opbeans-go:3000', - type: 'external', - }, - }, - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-go:3000/api/orders', - }, - }, - id: 'fc107f7b556eb49b', - type: 'external', - }, - }, - { - parent: { - id: '975c8d5bfd1dd20b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', - }, - timestamp: { - us: 1584975868787174, - }, - span: { - duration: { - us: 16250, - }, - subtype: 'http', - destination: { - service: { - resource: 'opbeans-python:3000', - name: 'http://opbeans-python:3000', - type: 'external', - }, - }, - name: 'I am his 👇🏻 parent 😡', - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-python:3000/api/orders', - }, - }, - id: 'daae24d83c269918', - type: 'external', - }, - }, - { - container: { - id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - parent: { - id: '6fb0ff7365b87298', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.790Z', - service: { - node: { - name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', - }, - transaction: { - id: '6fb0ff7365b87298', - }, - timestamp: { - us: 1584975868781000, - }, - span: { - duration: { - us: 2519, - }, - subtype: 'postgresql', - name: 'I am using my parents skew 😇', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - action: 'query', - id: 'c9407abb4d08ead1', - type: 'db', - sync: true, - db: { - statement: - 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', - type: 'sql', - }, - }, - }, - ], - exceedsMax: false, - errorDocs: [], -} as TraceAPIResponse; - -export const inferredSpans = { - traceDocs: [ - { - container: { - id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - source: { - ip: '172.18.0.8', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/products/2', - scheme: 'http', - port: 3000, - domain: '172.18.0.7', - full: 'http://172.18.0.7:3000/api/products/2', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.786Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - client: { - ip: '172.18.0.8', - }, - http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.18.0.7:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.18.0.8', - }, - }, - response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Thu, 09 Apr 2020 11:36:01 GMT'], - 'Content-Type': ['application/json;charset=UTF-8'], - }, - status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', - }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', - name: 'Other', - device: { - name: 'Other', - }, - }, - transaction: { - duration: { - us: 237537, - }, - result: 'HTTP 2xx', - name: 'APIRestController#product', - span_count: { - dropped: 0, - started: 3, - }, - id: 'f2387d37260d00bd', - type: 'request', - sampled: true, - }, - timestamp: { - us: 1586432160786001, - }, - }, - { - container: { - id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - parent: { - id: 'f2387d37260d00bd', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.810Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - span: { - duration: { - us: 204574, - }, - subtype: 'inferred', - name: 'ServletInvocableHandlerMethod#invokeAndHandle', - id: 'a5df600bd7bd5e38', - type: 'app', - }, - timestamp: { - us: 1586432160810441, - }, - }, - { - container: { - id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - parent: { - id: 'a5df600bd7bd5e38', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - type: 'apm-server', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.810Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160810441, - }, - span: { - duration: { - us: 102993, - }, - stacktrace: [ - { - library_frame: true, - exclude_from_grouping: false, - filename: 'InvocableHandlerMethod.java', - line: { - number: -1, - }, - function: 'doInvoke', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'InvocableHandlerMethod.java', - line: { - number: -1, - }, - function: 'invokeForRequest', - }, - ], - subtype: 'inferred', - name: 'APIRestController#product', - id: '808dc34fc41ce522', - type: 'app', - }, - }, - { - container: { - id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - parent: { - id: 'f2387d37260d00bd', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - processor: { - name: 'transaction', - event: 'span', - }, - labels: { - productId: '2', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.832Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160832300, - }, - span: { - duration: { - us: 99295, - }, - name: 'OpenTracing product span', - id: '41226ae63af4f235', - type: 'unknown', - }, - child: { id: ['8d80de06aa11a6fc'] }, - }, - { - container: { - id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - parent: { - id: '808dc34fc41ce522', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.859Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160859600, - }, - span: { - duration: { - us: 53835, - }, - subtype: 'inferred', - name: 'Loader#executeQueryStatement', - id: '8d80de06aa11a6fc', - type: 'app', - }, - }, - { - container: { - id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - parent: { - id: '41226ae63af4f235', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - destination: { - address: 'postgres', - port: 5432, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.903Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160903236, - }, - span: { - duration: { - us: 10211, - }, - subtype: 'postgresql', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - name: 'SELECT FROM products', - action: 'query', - id: '3708d5623658182f', - type: 'db', - db: { - statement: - 'select product0_.id as col_0_0_, product0_.sku as col_1_0_, product0_.name as col_2_0_, product0_.description as col_3_0_, product0_.cost as col_4_0_, product0_.selling_price as col_5_0_, product0_.stock as col_6_0_, producttyp1_.id as col_7_0_, producttyp1_.name as col_8_0_, (select sum(orderline2_.amount) from order_lines orderline2_ where orderline2_.product_id=product0_.id) as col_9_0_ from products product0_ left outer join product_types producttyp1_ on product0_.type_id=producttyp1_.id where product0_.id=?', - type: 'sql', - user: { - name: 'postgres', - }, - }, - }, - }, - { - container: { - id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - parent: { - id: '41226ae63af4f235', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - destination: { - address: 'postgres', - port: 5432, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.859Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160859508, - }, - span: { - duration: { - us: 4503, - }, - subtype: 'postgresql', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - name: 'empty query', - action: 'query', - id: '9871cfd612368932', - type: 'db', - db: { - rows_affected: 0, - statement: '(empty query)', - type: 'sql', - user: { - name: 'postgres', - }, - }, - }, - }, - ], - exceedsMax: false, - errorDocs: [], -} as TraceAPIResponse; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts new file mode 100644 index 0000000000000..6cca3726a7d00 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts @@ -0,0 +1,5865 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Location } from 'history'; +import type { ApmUrlParams } from '../../../../../context/url_params_context/types'; +import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; + +export const location = { + pathname: '/services/opbeans-go/transactions/view', + search: + '?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=service.name%253A%2520%2522opbeans-java%2522%2520or%2520service.name%2520%253A%2520%2522opbeans-go%2522&traceId=513d33fafe99bbe6134749310c9b5322&transactionId=975c8d5bfd1dd20b&transactionName=GET%20%2Fapi%2Forders&transactionType=request', + hash: '', +} as Location; + +type TraceAPIResponse = APIReturnType<'GET /internal/apm/traces/{traceId}'>; + +export const urlParams = { + start: '2020-03-22T15:16:38.742Z', + end: '2020-03-23T15:16:38.742Z', + rangeFrom: 'now-24h', + rangeTo: 'now', + refreshPaused: true, + refreshInterval: 0, + page: 0, + transactionId: '975c8d5bfd1dd20b', + traceId: '513d33fafe99bbe6134749310c9b5322', + transactionName: 'GET /api/orders', + transactionType: 'request', + processorEvent: 'transaction', + serviceName: 'opbeans-go', +} as ApmUrlParams; + +export const simpleTrace = { + traceDocs: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 46, + }, + }, + source: { + ip: '172.19.0.13', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', + }, + language: { + name: 'Java', + version: '10.0.2', + }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13', + }, + body: { + original: '[REDACTED]', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + client: { + ip: '172.19.0.13', + }, + transaction: { + duration: { + us: 18842, + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other', + }, + }, + timestamp: { + us: 1584975868785000, + }, + }, + { + parent: { + id: 'fc107f7b556eb49b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + duration: { + us: 16597, + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + timestamp: { + us: 1584975868787052, + }, + }, + { + parent: { + id: 'daae24d83c269918', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + timestamp: { + us: 1584975868788603, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', + }, + language: { + name: 'python', + version: '3.6.10', + }, + version: 'None', + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648, + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1, + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + parent: { + id: '49809ad3c26adf74', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 44, + }, + }, + destination: { + address: 'opbeans-go', + port: 3000, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', + }, + language: { + name: 'Java', + version: '10.0.2', + }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + connection: { + hash: "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", + }, + transaction: { + id: '49809ad3c26adf74', + }, + timestamp: { + us: 1584975868785273, + }, + span: { + duration: { + us: 17530, + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external', + }, + }, + http: { + response: { + status_code: 200, + }, + url: { + original: 'http://opbeans-go:3000/api/orders', + }, + }, + id: 'fc107f7b556eb49b', + type: 'external', + }, + }, + { + parent: { + id: '975c8d5bfd1dd20b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + id: '975c8d5bfd1dd20b', + }, + timestamp: { + us: 1584975868787174, + }, + span: { + duration: { + us: 16250, + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external', + }, + }, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200, + }, + url: { + original: 'http://opbeans-python:3000/api/orders', + }, + }, + id: 'daae24d83c269918', + type: 'external', + }, + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', + }, + language: { + name: 'python', + version: '3.6.10', + }, + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + }, + timestamp: { + us: 1584975868790080, + }, + span: { + duration: { + us: 2519, + }, + subtype: 'postgresql', + name: 'SELECT FROM opbeans_order', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', + }, + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql', + }, + }, + }, + ], + exceedsMax: false, + errorDocs: [], +} as TraceAPIResponse; + +export const manyChildrenWithSameLength = { + exceedsMax: false, + traceDocs: [ + { + container: { + id: '46721e28e45ec1926798491069d8585865b031b4eaa9800e35d06fef6be5e170', + }, + kubernetes: { + pod: { + uid: '900f3cac-eb7c-4308-9376-f644f173c3ee', + }, + }, + process: { + args: ['-C', 'config/puma.rb'], + pid: 38, + title: '/usr/local/bundle/bin/puma', + }, + agent: { + name: 'ruby', + version: '4.3.0', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/products/3', + scheme: 'http', + port: 3000, + domain: '10.15.245.224', + full: 'http://10.15.245.224:3000/api/products/3', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'projects/8560181848/machineTypes/n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-zvs6p', + id: '69a7066f-46d2-42c4-a4cc-8400f60bf2b5', + ephemeral_id: '0ab88569-c301-40e9-8e78-cac7c1dac2bc', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.536Z', + ecs: { + version: '1.11.0', + }, + service: { + node: { + name: '46721e28e45ec1926798491069d8585865b031b4eaa9800e35d06fef6be5e170', + }, + environment: 'production', + framework: { + name: 'Ruby on Rails', + version: '6.1.4.1', + }, + name: 'opbeans-ruby', + runtime: { + name: 'ruby', + version: '2.7.3', + }, + language: { + name: 'ruby', + version: '2.7.3', + }, + version: '2021-10-14 17:49:53', + }, + host: { + os: { + platform: 'linux', + }, + ip: '10.12.0.22', + architecture: 'x86_64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + Version: ['HTTP/1.1'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['10.15.245.224:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'GET', + env: { + GATEWAY_INTERFACE: 'CGI/1.2', + ORIGINAL_FULLPATH: '/api/products/3', + SERVER_PORT: '3000', + SERVER_PROTOCOL: 'HTTP/1.1', + REMOTE_ADDR: '10.12.6.45', + REQUEST_URI: '/api/products/3', + ORIGINAL_SCRIPT_NAME: '', + SERVER_SOFTWARE: 'puma 5.5.0 Zawgyi', + QUERY_STRING: '', + SCRIPT_NAME: '', + REQUEST_METHOD: 'GET', + SERVER_NAME: '10.15.245.224', + REQUEST_PATH: '/api/products/3', + PATH_INFO: '/api/products/3', + ROUTES_9240_SCRIPT_NAME: '', + }, + body: { + original: '[SKIPPED]', + }, + }, + response: { + headers: { + 'Content-Type': ['application/json;charset=UTF-8'], + }, + status_code: 500, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + event: { + ingested: '2021-10-19T13:57:12.417144879Z', + outcome: 'failure', + }, + transaction: { + duration: { + us: 13359, + }, + result: 'HTTP 5xx', + name: 'Rack', + span_count: { + dropped: 0, + started: 1, + }, + id: '9a7f717439921d39', + type: 'request', + sampled: true, + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Python aiohttp', + device: { + name: 'Other', + type: 'Other', + }, + version: '3.3.2', + }, + timestamp: { + us: 1634651822536408, + }, + }, + { + container: { + id: 'e7b69f99cb7523bedea6d7c97b684cf4b7ff458d0cba1efb1ac843300b3bf3c7', + }, + kubernetes: { + pod: { + uid: 'c5169b50-f3b3-4693-8e4b-150fca17c333', + name: 'opbeans-go-5d795ddf6b-rhlvf', + }, + }, + parent: { + id: '4eeaa6dfbfd047cd', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + source: { + ip: '10.12.0.22', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + type: 'apm-server', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + ecs: { + version: '1.11.0', + }, + host: { + os: { + platform: 'linux', + }, + ip: '10.12.0.14', + architecture: 'amd64', + }, + client: { + ip: '10.12.0.22', + }, + event: { + ingested: '2021-10-19T13:57:05.413190788Z', + outcome: 'failure', + }, + user_agent: { + original: 'http.rb/5.0.2', + name: 'Other', + device: { + name: 'Generic Feature Phone', + type: 'Other', + }, + }, + timestamp: { + us: 1634651822536408, + }, + process: { + args: [ + '/opbeans-go', + '-log-level=debug', + '-log-json', + '-listen=:3000', + '-frontend=/opbeans-frontend', + '-db=postgres:', + '-cache=redis://redis-master:6379', + ], + pid: 1, + title: 'opbeans-go', + ppid: 0, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/products/3', + scheme: 'http', + port: 3000, + domain: 'opbeans', + full: 'http://opbeans:3000/api/products/3', + }, + '@timestamp': '2021-10-19T13:57:02.536Z', + service: { + node: { + name: 'e7b69f99cb7523bedea6d7c97b684cf4b7ff458d0cba1efb1ac843300b3bf3c7', + }, + environment: 'testing', + framework: { + name: 'gin', + version: 'v1.7.3', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.17.2', + }, + language: { + name: 'go', + version: 'go1.17.2', + }, + version: '2021-10-14 17:49:50', + }, + http: { + request: { + headers: { + Connection: ['close'], + 'User-Agent': ['http.rb/5.0.2'], + 'Elastic-Apm-Traceparent': [ + '00-d5e80ae688f1fef91533f02dd2bdc769-4eeaa6dfbfd047cd-01', + ], + Tracestate: ['es=s:1.0'], + Traceparent: [ + '00-d5e80ae688f1fef91533f02dd2bdc769-4eeaa6dfbfd047cd-01', + ], + }, + method: 'GET', + }, + response: { + headers: { + Date: ['Tue, 19 Oct 2021 13:57:02 GMT'], + 'Content-Type': ['application/json;charset=UTF-8'], + }, + status_code: 500, + }, + version: '1.1', + }, + transaction: { + result: 'HTTP 5xx', + duration: { + us: 13359, + }, + name: 'GET /api/products/:id', + id: '9f50f43e924d0b46', + span_count: { + dropped: 0, + started: 3, + }, + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '015d1127421e2c3d42a0fb031fc75e989813f58973143b6c7e33dca6ccc6f31b', + }, + parent: { + id: '8d099ab4fcec4ab9', + }, + kubernetes: { + pod: { + uid: '459a6abf-198e-4107-b4dd-b0ae826755ab', + name: 'opbeans-go-nsn-69b89c4598-xsvgh', + }, + }, + agent: { + name: 'go', + version: '1.14.0', + }, + source: { + ip: '10.12.0.14', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-zvs6p', + id: '69a7066f-46d2-42c4-a4cc-8400f60bf2b5', + ephemeral_id: '0ab88569-c301-40e9-8e78-cac7c1dac2bc', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + ecs: { + version: '1.11.0', + }, + host: { + os: { + platform: 'linux', + }, + ip: '10.12.0.13', + architecture: 'amd64', + }, + client: { + ip: '10.12.0.22', + }, + event: { + ingested: '2021-10-19T13:57:08.267103644Z', + outcome: 'failure', + }, + user_agent: { + original: 'http.rb/5.0.2', + name: 'Other', + device: { + name: 'Generic Feature Phone', + type: 'Other', + }, + }, + timestamp: { + us: 1634651822536408, + }, + process: { + args: [ + '/opbeans-go', + '-log-level=debug', + '-log-json', + '-listen=:3000', + '-frontend=/opbeans-frontend', + '-db=postgres:', + '-cache=redis://redis-master:6379', + ], + pid: 1, + title: 'opbeans-go', + ppid: 0, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/products/3', + scheme: 'http', + port: 3000, + domain: 'opbeans', + full: 'http://opbeans:3000/api/products/3', + }, + '@timestamp': '2021-10-19T13:57:02.536Z', + service: { + node: { + name: '015d1127421e2c3d42a0fb031fc75e989813f58973143b6c7e33dca6ccc6f31b', + }, + environment: 'testing', + framework: { + name: 'gin', + version: 'v1.7.3', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.17.2', + }, + language: { + name: 'go', + version: 'go1.17.2', + }, + version: '2021-10-14 17:49:50', + }, + http: { + request: { + headers: { + 'User-Agent': ['http.rb/5.0.2'], + 'X-Forwarded-For': ['10.12.0.22'], + 'Accept-Encoding': ['gzip'], + 'Elastic-Apm-Traceparent': [ + '00-d5e80ae688f1fef91533f02dd2bdc769-8d099ab4fcec4ab9-01', + ], + Tracestate: ['es=s:1.0'], + Traceparent: [ + '00-d5e80ae688f1fef91533f02dd2bdc769-8d099ab4fcec4ab9-01', + ], + }, + method: 'GET', + }, + response: { + headers: { + Date: ['Tue, 19 Oct 2021 13:57:02 GMT'], + 'Content-Type': ['application/json;charset=UTF-8'], + }, + status_code: 500, + }, + version: '1.1', + }, + transaction: { + result: 'HTTP 5xx', + duration: { + us: 13359, + }, + name: 'GET /api/products/:id', + span_count: { + dropped: 0, + started: 3, + }, + id: 'b7801be83bcdc972', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '59036ecb70908dfec4e03edc477f6875d08677871b4af0db3144373802d00cb1', + }, + kubernetes: { + pod: { + uid: '878bab2a-1309-44ae-a0e2-c98a0b187da1', + name: 'opbeans-java-5f45d77dd8-h8bnb', + }, + }, + parent: { + id: '35e3637e26919055', + }, + agent: { + name: 'java', + ephemeral_id: '75e36588-9adb-4bb0-bfee-a333b1c57e67', + version: 'unknown', + }, + source: { + ip: '10.12.0.13', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + ecs: { + version: '1.11.0', + }, + host: { + os: { + platform: 'Linux', + }, + ip: '10.12.0.15', + architecture: 'amd64', + }, + client: { + ip: '10.12.0.22', + }, + event: { + ingested: '2021-10-19T13:57:10.382829210Z', + outcome: 'failure', + }, + user_agent: { + original: 'http.rb/5.0.2', + name: 'Other', + device: { + name: 'Generic Feature Phone', + type: 'Other', + }, + }, + timestamp: { + us: 1634651822536408, + }, + process: { + pid: 7, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/products/3', + scheme: 'http', + port: 3000, + domain: 'opbeans', + full: 'http://opbeans:3000/api/products/3', + }, + '@timestamp': '2021-10-19T13:57:02.536Z', + service: { + node: { + name: '59036ecb70908dfec4e03edc477f6875d08677871b4af0db3144373802d00cb1', + }, + environment: 'production', + framework: { + name: 'Spring Web MVC', + version: '5.0.6.RELEASE', + }, + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.11', + }, + language: { + name: 'Java', + version: '11.0.11', + }, + version: '2021-10-14 17:49:52', + }, + http: { + request: { + headers: { + 'User-Agent': ['http.rb/5.0.2'], + 'X-Forwarded-For': ['10.12.0.22, 10.12.0.14'], + Host: ['opbeans:3000'], + 'Accept-Encoding': ['gzip'], + 'Elastic-Apm-Traceparent': [ + '00-d5e80ae688f1fef91533f02dd2bdc769-35e3637e26919055-01', + ], + Tracestate: ['es=s:1.0'], + Traceparent: [ + '00-d5e80ae688f1fef91533f02dd2bdc769-35e3637e26919055-01', + ], + }, + method: 'GET', + }, + response: { + status_code: 500, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + transaction: { + duration: { + us: 13359, + }, + result: 'HTTP 5xx', + name: 'APIRestController#product', + id: '2c30263c4ad8fe8b', + span_count: { + dropped: 0, + started: 3, + }, + type: 'request', + sampled: true, + }, + }, + { + parent: { + id: '9a7f717439921d39', + }, + agent: { + name: 'ruby', + version: '4.3.0', + }, + destination: { + address: 'opbeans', + port: 3000, + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'projects/8560181848/machineTypes/n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + ecs: { + version: '1.11.0', + }, + event: { + outcome: 'failure', + }, + timestamp: { + us: 1634651822536408, + }, + processor: { + name: 'transaction', + event: 'span', + }, + url: { + original: 'http://opbeans:3000/api/products/3', + }, + '@timestamp': '2021-10-19T13:57:02.536Z', + service: { + environment: 'production', + name: 'opbeans-ruby', + }, + http: { + request: { + method: 'GET', + }, + response: { + status_code: 500, + }, + }, + transaction: { + id: '9a7f717439921d39', + }, + span: { + duration: { + us: 13359, + }, + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'elastic_apm.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-4.3.0/lib/elastic_apm.rb', + line: { + number: 235, + }, + function: 'tap', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'elastic_apm.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-4.3.0/lib/elastic_apm.rb', + line: { + number: 235, + }, + function: 'start_span', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'elastic_apm.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-4.3.0/lib/elastic_apm.rb', + line: { + number: 287, + }, + function: 'with_span', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'elastic_apm/spies/http.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-4.3.0/lib/elastic_apm/spies/http.rb', + line: { + number: 45, + }, + function: 'perform', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/usr/local/bundle/gems/http-5.0.2/lib/http/client.rb', + filename: 'http/client.rb', + line: { + number: 31, + }, + function: 'request', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'http/chainable.rb', + abs_path: '/usr/local/bundle/gems/http-5.0.2/lib/http/chainable.rb', + line: { + number: 75, + }, + function: 'request', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'http/chainable.rb', + abs_path: '/usr/local/bundle/gems/http-5.0.2/lib/http/chainable.rb', + line: { + number: 20, + }, + function: 'get', + }, + { + exclude_from_grouping: false, + filename: 'opbeans_shuffle.rb', + abs_path: '/app/lib/opbeans_shuffle.rb', + line: { + number: 23, + context: ' resp = HTTP.get("#{lucky_winner}#{path}")\n', + }, + function: 'block in call', + context: { + pre: ['\n', ' Timeout.timeout(15) do\n'], + post: ['\n', ' [\n'], + }, + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'timeout.rb', + abs_path: '/usr/local/lib/ruby/2.7.0/timeout.rb', + line: { + number: 95, + }, + function: 'block in timeout', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'timeout.rb', + abs_path: '/usr/local/lib/ruby/2.7.0/timeout.rb', + line: { + number: 33, + }, + function: 'block in catch', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/usr/local/lib/ruby/2.7.0/timeout.rb', + filename: 'timeout.rb', + line: { + number: 33, + }, + function: 'catch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'timeout.rb', + abs_path: '/usr/local/lib/ruby/2.7.0/timeout.rb', + line: { + number: 33, + }, + function: 'catch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'timeout.rb', + abs_path: '/usr/local/lib/ruby/2.7.0/timeout.rb', + line: { + number: 110, + }, + function: 'timeout', + }, + { + exclude_from_grouping: false, + filename: 'opbeans_shuffle.rb', + abs_path: '/app/lib/opbeans_shuffle.rb', + line: { + number: 22, + context: ' Timeout.timeout(15) do\n', + }, + function: 'call', + context: { + pre: [' end\n', '\n'], + post: [ + ' resp = HTTP.get("#{lucky_winner}#{path}")\n', + '\n', + ], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'elastic_apm/middleware.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-4.3.0/lib/elastic_apm/middleware.rb', + line: { + number: 36, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rails/engine.rb', + abs_path: + '/usr/local/bundle/gems/railties-6.1.4.1/lib/rails/engine.rb', + line: { + number: 539, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/configuration.rb', + abs_path: + '/usr/local/bundle/gems/puma-5.5.0/lib/puma/configuration.rb', + line: { + number: 249, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/usr/local/bundle/gems/puma-5.5.0/lib/puma/request.rb', + filename: 'puma/request.rb', + line: { + number: 77, + }, + function: 'block in handle_request', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/thread_pool.rb', + abs_path: + '/usr/local/bundle/gems/puma-5.5.0/lib/puma/thread_pool.rb', + line: { + number: 340, + }, + function: 'with_force_shutdown', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/request.rb', + abs_path: '/usr/local/bundle/gems/puma-5.5.0/lib/puma/request.rb', + line: { + number: 76, + }, + function: 'handle_request', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-5.5.0/lib/puma/server.rb', + line: { + number: 447, + }, + function: 'process_client', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/thread_pool.rb', + abs_path: + '/usr/local/bundle/gems/puma-5.5.0/lib/puma/thread_pool.rb', + line: { + number: 147, + }, + function: 'block in spawn_thread', + }, + ], + subtype: 'http', + destination: { + service: { + resource: 'opbeans:3000', + name: 'http', + type: 'external', + }, + }, + name: 'GET opbeans', + http: { + method: 'GET', + response: { + status_code: 500, + }, + }, + 'http.url.original': 'http://opbeans:3000/api/products/3', + id: '4eeaa6dfbfd047cd', + type: 'external', + }, + }, + { + parent: { + id: '9f50f43e924d0b46', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + destination: { + address: 'opbeans', + port: 3000, + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + ecs: { + version: '1.11.0', + }, + event: { + outcome: 'failure', + }, + timestamp: { + us: 1634651822536408, + }, + processor: { + name: 'transaction', + event: 'span', + }, + url: { + original: 'http://opbeans:3000/api/products/3', + }, + '@timestamp': '2021-10-19T13:57:02.536Z', + service: { + environment: 'testing', + name: 'opbeans-go', + }, + http: { + response: { + status_code: 500, + }, + }, + transaction: { + id: '9f50f43e924d0b46', + }, + span: { + duration: { + us: 13359, + }, + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'span.go', + abs_path: '/go/pkg/mod/go.elastic.co/apm@v1.14.0/span.go', + line: { + number: 334, + }, + module: 'go.elastic.co/apm', + function: '(*Span).End', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'client.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmhttp@v1.14.0/client.go', + line: { + number: 198, + }, + module: 'go.elastic.co/apm/module/apmhttp', + function: '(*responseBody).endSpan', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'client.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmhttp@v1.14.0/client.go', + line: { + number: 187, + }, + function: '(*responseBody).Read', + module: 'go.elastic.co/apm/module/apmhttp', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'reverseproxy.go', + abs_path: '/usr/local/go/src/net/http/httputil/reverseproxy.go', + line: { + number: 461, + }, + module: 'net/http/httputil', + function: '(*ReverseProxy).copyBuffer', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'reverseproxy.go', + abs_path: '/usr/local/go/src/net/http/httputil/reverseproxy.go', + line: { + number: 449, + }, + module: 'net/http/httputil', + function: '(*ReverseProxy).copyResponse', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'reverseproxy.go', + abs_path: '/usr/local/go/src/net/http/httputil/reverseproxy.go', + line: { + number: 338, + }, + module: 'net/http/httputil', + function: '(*ReverseProxy).ServeHTTP', + }, + { + exclude_from_grouping: false, + filename: 'main.go', + abs_path: '/src/opbeans-go/main.go', + line: { + number: 196, + }, + module: 'main', + function: 'Main.func2', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + filename: 'context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + exclude_from_grouping: false, + filename: 'main.go', + abs_path: '/src/opbeans-go/main.go', + line: { + number: 174, + }, + module: 'main', + function: 'Main.func1', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + exclude_from_grouping: false, + filename: 'logger.go', + abs_path: '/src/opbeans-go/logger.go', + line: { + number: 36, + }, + module: 'main', + function: 'logrusMiddleware', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'middleware.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmgin@v1.14.0/middleware.go', + line: { + number: 98, + }, + module: 'go.elastic.co/apm/module/apmgin', + function: '(*middleware).handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'cache.go', + abs_path: + '/go/pkg/mod/github.com/gin-contrib/cache@v1.1.0/cache.go', + line: { + number: 128, + }, + module: 'github.com/gin-contrib/cache', + function: 'Cache.func1', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'gin.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + line: { + number: 489, + }, + function: '(*Engine).handleHTTPRequest', + module: 'github.com/gin-gonic/gin', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'gin.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + line: { + number: 445, + }, + function: '(*Engine).ServeHTTP', + module: 'github.com/gin-gonic/gin', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'server.go', + abs_path: '/usr/local/go/src/net/http/server.go', + line: { + number: 2878, + }, + module: 'net/http', + function: 'serverHandler.ServeHTTP', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'server.go', + abs_path: '/usr/local/go/src/net/http/server.go', + line: { + number: 1929, + }, + module: 'net/http', + function: '(*conn).serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'asm_amd64.s', + abs_path: '/usr/local/go/src/runtime/asm_amd64.s', + line: { + number: 1581, + }, + module: 'runtime', + function: 'goexit', + }, + ], + subtype: 'http', + name: 'GET opbeans:3000', + destination: { + service: { + resource: 'opbeans:3000', + name: 'http://opbeans:3000', + type: 'external', + }, + }, + http: { + response: { + status_code: 500, + }, + }, + 'http.url.original': 'http://opbeans:3000/api/products/3', + id: '8d099ab4fcec4ab9', + type: 'external', + }, + }, + { + parent: { + id: '86c43ac014573747', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + processor: { + name: 'transaction', + event: 'span', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + type: 'apm-server', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.539Z', + ecs: { + version: '1.11.0', + }, + service: { + environment: 'testing', + name: 'opbeans-go', + }, + event: { + outcome: 'unknown', + }, + transaction: { + id: '9f50f43e924d0b46', + }, + span: { + duration: { + us: 13359, + }, + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'span.go', + abs_path: '/go/pkg/mod/go.elastic.co/apm@v1.14.0/span.go', + line: { + number: 334, + }, + module: 'go.elastic.co/apm', + function: '(*Span).End', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'clienttrace.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmhttp@v1.14.0/clienttrace.go', + line: { + number: 130, + }, + module: 'go.elastic.co/apm/module/apmhttp', + function: 'withClientTrace.func8', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'transport.go', + abs_path: '/usr/local/go/src/net/http/transport.go', + line: { + number: 2272, + }, + function: '(*persistConn).readResponse', + module: 'net/http', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'transport.go', + abs_path: '/usr/local/go/src/net/http/transport.go', + line: { + number: 2102, + }, + function: '(*persistConn).readLoop', + module: 'net/http', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'asm_amd64.s', + abs_path: '/usr/local/go/src/runtime/asm_amd64.s', + line: { + number: 1581, + }, + module: 'runtime', + function: 'goexit', + }, + ], + subtype: 'http', + name: 'Request', + action: 'request', + id: '997cdcc26a60d0ad', + type: 'external', + }, + timestamp: { + us: 1634651822536408, + }, + }, + { + parent: { + id: 'b7801be83bcdc972', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + destination: { + address: 'opbeans', + port: 3000, + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-zvs6p', + id: '69a7066f-46d2-42c4-a4cc-8400f60bf2b5', + ephemeral_id: '0ab88569-c301-40e9-8e78-cac7c1dac2bc', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + ecs: { + version: '1.11.0', + }, + event: { + outcome: 'failure', + }, + timestamp: { + us: 1634651822536408, + }, + processor: { + name: 'transaction', + event: 'span', + }, + url: { + original: 'http://opbeans:3000/api/products/3', + }, + '@timestamp': '2021-10-19T13:57:02.539Z', + service: { + environment: 'testing', + name: 'opbeans-go', + }, + http: { + response: { + status_code: 500, + }, + }, + transaction: { + id: 'b7801be83bcdc972', + }, + span: { + duration: { + us: 13359, + }, + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'span.go', + abs_path: '/go/pkg/mod/go.elastic.co/apm@v1.14.0/span.go', + line: { + number: 334, + }, + module: 'go.elastic.co/apm', + function: '(*Span).End', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'client.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmhttp@v1.14.0/client.go', + line: { + number: 198, + }, + module: 'go.elastic.co/apm/module/apmhttp', + function: '(*responseBody).endSpan', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'client.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmhttp@v1.14.0/client.go', + line: { + number: 187, + }, + module: 'go.elastic.co/apm/module/apmhttp', + function: '(*responseBody).Read', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'reverseproxy.go', + abs_path: '/usr/local/go/src/net/http/httputil/reverseproxy.go', + line: { + number: 461, + }, + module: 'net/http/httputil', + function: '(*ReverseProxy).copyBuffer', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'reverseproxy.go', + abs_path: '/usr/local/go/src/net/http/httputil/reverseproxy.go', + line: { + number: 449, + }, + module: 'net/http/httputil', + function: '(*ReverseProxy).copyResponse', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'reverseproxy.go', + abs_path: '/usr/local/go/src/net/http/httputil/reverseproxy.go', + line: { + number: 338, + }, + module: 'net/http/httputil', + function: '(*ReverseProxy).ServeHTTP', + }, + { + exclude_from_grouping: false, + filename: 'main.go', + abs_path: '/src/opbeans-go/main.go', + line: { + number: 196, + }, + module: 'main', + function: 'Main.func2', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + exclude_from_grouping: false, + filename: 'main.go', + abs_path: '/src/opbeans-go/main.go', + line: { + number: 174, + }, + module: 'main', + function: 'Main.func1', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + exclude_from_grouping: false, + filename: 'logger.go', + abs_path: '/src/opbeans-go/logger.go', + line: { + number: 36, + }, + module: 'main', + function: 'logrusMiddleware', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'middleware.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmgin@v1.14.0/middleware.go', + line: { + number: 98, + }, + function: '(*middleware).handle', + module: 'go.elastic.co/apm/module/apmgin', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/go/pkg/mod/github.com/gin-contrib/cache@v1.1.0/cache.go', + filename: 'cache.go', + line: { + number: 128, + }, + module: 'github.com/gin-contrib/cache', + function: 'Cache.func1', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + function: '(*Context).Next', + module: 'github.com/gin-gonic/gin', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'gin.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + line: { + number: 489, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Engine).handleHTTPRequest', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'gin.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + line: { + number: 445, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Engine).ServeHTTP', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/usr/local/go/src/net/http/server.go', + filename: 'server.go', + line: { + number: 2878, + }, + module: 'net/http', + function: 'serverHandler.ServeHTTP', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/usr/local/go/src/net/http/server.go', + filename: 'server.go', + line: { + number: 1929, + }, + module: 'net/http', + function: '(*conn).serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'asm_amd64.s', + abs_path: '/usr/local/go/src/runtime/asm_amd64.s', + line: { + number: 1581, + }, + module: 'runtime', + function: 'goexit', + }, + ], + subtype: 'http', + name: 'GET opbeans:3000', + destination: { + service: { + resource: 'opbeans:3000', + name: 'http://opbeans:3000', + type: 'external', + }, + }, + http: { + response: { + status_code: 500, + }, + }, + 'http.url.original': 'http://opbeans:3000/api/products/3', + id: '35e3637e26919055', + type: 'external', + }, + }, + { + parent: { + id: '84749ec73b1268b3', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + processor: { + name: 'transaction', + event: 'span', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-zvs6p', + id: '69a7066f-46d2-42c4-a4cc-8400f60bf2b5', + type: 'apm-server', + ephemeral_id: '0ab88569-c301-40e9-8e78-cac7c1dac2bc', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.539Z', + ecs: { + version: '1.11.0', + }, + service: { + environment: 'testing', + name: 'opbeans-go', + }, + event: { + outcome: 'unknown', + }, + transaction: { + id: 'b7801be83bcdc972', + }, + span: { + duration: { + us: 13359, + }, + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'span.go', + abs_path: '/go/pkg/mod/go.elastic.co/apm@v1.14.0/span.go', + line: { + number: 334, + }, + module: 'go.elastic.co/apm', + function: '(*Span).End', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'clienttrace.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmhttp@v1.14.0/clienttrace.go', + line: { + number: 130, + }, + module: 'go.elastic.co/apm/module/apmhttp', + function: 'withClientTrace.func8', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'transport.go', + abs_path: '/usr/local/go/src/net/http/transport.go', + line: { + number: 2272, + }, + module: 'net/http', + function: '(*persistConn).readResponse', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'transport.go', + abs_path: '/usr/local/go/src/net/http/transport.go', + line: { + number: 2102, + }, + module: 'net/http', + function: '(*persistConn).readLoop', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'asm_amd64.s', + abs_path: '/usr/local/go/src/runtime/asm_amd64.s', + line: { + number: 1581, + }, + module: 'runtime', + function: 'goexit', + }, + ], + subtype: 'http', + name: 'Request', + action: 'request', + id: 'a9b4d44c3d699cbb', + type: 'external', + }, + timestamp: { + us: 1634651822536408, + }, + }, + { + parent: { + id: '2c30263c4ad8fe8b', + }, + agent: { + name: 'java', + ephemeral_id: '75e36588-9adb-4bb0-bfee-a333b1c57e67', + version: 'unknown', + }, + processor: { + name: 'transaction', + event: 'span', + }, + labels: { + productId: '3', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.540Z', + ecs: { + version: '1.11.0', + }, + service: { + environment: 'production', + name: 'opbeans-java', + }, + event: { + outcome: 'success', + }, + transaction: { + id: '2c30263c4ad8fe8b', + }, + span: { + duration: { + us: 13359, + }, + name: 'OpenTracing product span', + id: 'd22c1e48b2489017', + type: 'custom', + }, + timestamp: { + us: 1634651822536408, + }, + }, + { + parent: { + id: 'd22c1e48b2489017', + }, + agent: { + name: 'java', + ephemeral_id: '75e36588-9adb-4bb0-bfee-a333b1c57e67', + version: 'unknown', + }, + destination: { + address: 'db-postgresql', + port: 5432, + }, + processor: { + name: 'transaction', + event: 'span', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + type: 'apm-server', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.542Z', + ecs: { + version: '1.11.0', + }, + service: { + environment: 'production', + name: 'opbeans-java', + }, + event: { + outcome: 'success', + }, + transaction: { + id: '2c30263c4ad8fe8b', + }, + timestamp: { + us: 1634651822536408, + }, + span: { + duration: { + us: 13359, + }, + subtype: 'postgresql', + destination: { + service: { + resource: 'postgresql', + }, + }, + name: 'SELECT FROM products', + action: 'query', + id: '3851260ca4365f9e', + type: 'db', + db: { + instance: 'opbeans-java', + statement: + 'select product0_.id as col_0_0_, product0_.sku as col_1_0_, product0_.name as col_2_0_, product0_.description as col_3_0_, product0_.cost as col_4_0_, product0_.selling_price as col_5_0_, product0_.stock as col_6_0_, producttyp1_.id as col_7_0_, producttyp1_.name as col_8_0_, (select sum(orderline2_.amount) from order_lines orderline2_ where orderline2_.product_id=product0_.id) as col_9_0_ from products product0_ left outer join product_types producttyp1_ on product0_.type_id=producttyp1_.id where product0_.id=?', + type: 'sql', + user: { + name: 'elastic', + }, + }, + }, + }, + { + parent: { + id: '3851260ca4365f9e', + }, + agent: { + name: 'java', + ephemeral_id: '75e36588-9adb-4bb0-bfee-a333b1c57e67', + version: 'unknown', + }, + destination: { + address: 'db-postgresql', + port: 5432, + }, + processor: { + name: 'transaction', + event: 'span', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + type: 'apm-server', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.541Z', + ecs: { + version: '1.11.0', + }, + service: { + environment: 'production', + name: 'opbeans-java', + }, + event: { + outcome: 'success', + }, + transaction: { + id: '2c30263c4ad8fe8b', + }, + span: { + duration: { + us: 13359, + }, + subtype: 'postgresql', + destination: { + service: { + resource: 'postgresql', + }, + }, + name: 'empty query', + action: 'query', + id: '86c43ac014573747', + type: 'db', + db: { + rows_affected: 0, + instance: 'opbeans-java', + statement: '(empty query)', + type: 'sql', + user: { + name: 'elastic', + }, + }, + }, + timestamp: { + us: 1634651822536408, + }, + }, + { + parent: { + id: '997cdcc26a60d0ad', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + processor: { + name: 'transaction', + event: 'span', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + type: 'apm-server', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.548Z', + ecs: { + version: '1.11.0', + }, + service: { + environment: 'testing', + name: 'opbeans-go', + }, + event: { + outcome: 'unknown', + }, + transaction: { + id: '9f50f43e924d0b46', + }, + timestamp: { + us: 1634651822536408, + }, + span: { + duration: { + us: 13359, + }, + subtype: 'http', + name: 'Response', + action: 'response', + id: '84749ec73b1268b3', + type: 'external', + }, + }, + { + parent: { + id: 'a9b4d44c3d699cbb', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + processor: { + name: 'transaction', + event: 'span', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-zvs6p', + id: '69a7066f-46d2-42c4-a4cc-8400f60bf2b5', + type: 'apm-server', + ephemeral_id: '0ab88569-c301-40e9-8e78-cac7c1dac2bc', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.547Z', + ecs: { + version: '1.11.0', + }, + service: { + environment: 'testing', + name: 'opbeans-go', + }, + event: { + outcome: 'unknown', + }, + transaction: { + id: 'b7801be83bcdc972', + }, + span: { + duration: { + us: 13359, + }, + subtype: 'http', + name: 'Response', + action: 'response', + id: '04991f3b9d3696c5', + type: 'external', + }, + timestamp: { + us: 1634651822536408, + }, + }, + ], + errorDocs: [ + { + container: { + id: '59036ecb70908dfec4e03edc477f6875d08677871b4af0db3144373802d00cb1', + }, + kubernetes: { + pod: { + uid: '878bab2a-1309-44ae-a0e2-c98a0b187da1', + name: 'opbeans-java-5f45d77dd8-h8bnb', + }, + }, + parent: { + id: '2c30263c4ad8fe8b', + }, + agent: { + name: 'java', + ephemeral_id: '75e36588-9adb-4bb0-bfee-a333b1c57e67', + version: 'unknown', + }, + source: { + ip: '10.12.0.13', + }, + error: { + exception: [ + { + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'AbstractMessageConverterMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor', + line: { + number: 226, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'writeWithMessageConverters', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'RequestResponseBodyMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor', + line: { + number: 180, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HandlerMethodReturnValueHandlerComposite.java', + classname: + 'org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite', + line: { + number: 82, + }, + module: 'org.springframework.web.method.support', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ServletInvocableHandlerMethod.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod', + line: { + number: 119, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeAndHandle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 877, + }, + function: 'invokeHandlerMethod', + module: 'org.springframework.web.servlet.mvc.method.annotation', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 783, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractHandlerMethodAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter', + line: { + number: 87, + }, + module: 'org.springframework.web.servlet.mvc.method', + function: 'handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 991, + }, + function: 'doDispatch', + module: 'org.springframework.web.servlet', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 925, + }, + module: 'org.springframework.web.servlet', + function: 'doService', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 974, + }, + module: 'org.springframework.web.servlet', + function: 'processRequest', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 866, + }, + module: 'org.springframework.web.servlet', + function: 'doGet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 635, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 851, + }, + function: 'service', + module: 'org.springframework.web.servlet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 742, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 231, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'WsFilter.java', + classname: 'org.apache.tomcat.websocket.server.WsFilter', + line: { + number: 52, + }, + module: 'org.apache.tomcat.websocket.server', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestContextFilter.java', + classname: + 'org.springframework.web.filter.RequestContextFilter', + line: { + number: 99, + }, + function: 'doFilterInternal', + module: 'org.springframework.web.filter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: + 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpPutFormContentFilter.java', + classname: + 'org.springframework.web.filter.HttpPutFormContentFilter', + line: { + number: 109, + }, + function: 'doFilterInternal', + module: 'org.springframework.web.filter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: + 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HiddenHttpMethodFilter.java', + classname: + 'org.springframework.web.filter.HiddenHttpMethodFilter', + line: { + number: 81, + }, + function: 'doFilterInternal', + module: 'org.springframework.web.filter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: + 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CharacterEncodingFilter.java', + classname: + 'org.springframework.web.filter.CharacterEncodingFilter', + line: { + number: 200, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'OncePerRequestFilter.java', + classname: + 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardWrapperValve.java', + classname: 'org.apache.catalina.core.StandardWrapperValve', + line: { + number: 198, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'StandardContextValve.java', + classname: 'org.apache.catalina.core.StandardContextValve', + line: { + number: 96, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AuthenticatorBase.java', + classname: + 'org.apache.catalina.authenticator.AuthenticatorBase', + line: { + number: 496, + }, + module: 'org.apache.catalina.authenticator', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardHostValve.java', + classname: 'org.apache.catalina.core.StandardHostValve', + line: { + number: 140, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ErrorReportValve.java', + classname: 'org.apache.catalina.valves.ErrorReportValve', + line: { + number: 81, + }, + module: 'org.apache.catalina.valves', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardEngineValve.java', + classname: 'org.apache.catalina.core.StandardEngineValve', + line: { + number: 87, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CoyoteAdapter.java', + classname: 'org.apache.catalina.connector.CoyoteAdapter', + line: { + number: 342, + }, + module: 'org.apache.catalina.connector', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'Http11Processor.java', + classname: 'org.apache.coyote.http11.Http11Processor', + line: { + number: 803, + }, + module: 'org.apache.coyote.http11', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractProcessorLight.java', + classname: 'org.apache.coyote.AbstractProcessorLight', + line: { + number: 66, + }, + module: 'org.apache.coyote', + function: 'process', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'AbstractProtocol.java', + classname: + 'org.apache.coyote.AbstractProtocol$ConnectionHandler', + line: { + number: 790, + }, + module: 'org.apache.coyote', + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'NioEndpoint.java', + classname: + 'org.apache.tomcat.util.net.NioEndpoint$SocketProcessor', + line: { + number: 1468, + }, + module: 'org.apache.tomcat.util.net', + function: 'doRun', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'SocketProcessorBase.java', + classname: 'org.apache.tomcat.util.net.SocketProcessorBase', + line: { + number: 49, + }, + module: 'org.apache.tomcat.util.net', + function: 'run', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'TaskThread.java', + classname: + 'org.apache.tomcat.util.threads.TaskThread$WrappingRunnable', + line: { + number: 61, + }, + module: 'org.apache.tomcat.util.threads', + function: 'run', + }, + ], + message: + 'No converter found for return value of type: class com.sun.proxy.$Proxy158', + type: 'org.springframework.http.converter.HttpMessageNotWritableException', + }, + ], + id: '128f8ecf47bc8a800269ee6e5ac90008', + grouping_key: 'cc9272d7511c88a533ac41cc3e2ce54b', + grouping_name: + 'No converter found for return value of type: class com.sun.proxy.$Proxy158', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + ecs: { + version: '1.11.0', + }, + host: { + os: { + platform: 'Linux', + }, + ip: '10.12.0.15', + architecture: 'amd64', + }, + client: { + ip: '10.12.0.22', + }, + event: { + ingested: '2021-10-19T13:57:10.382394342Z', + }, + user_agent: { + original: 'http.rb/5.0.2', + name: 'Other', + device: { + name: 'Generic Feature Phone', + type: 'Other', + }, + }, + timestamp: { + us: 1634651822536408, + }, + process: { + pid: 7, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + message: + 'No converter found for return value of type: class com.sun.proxy.$Proxy158', + processor: { + name: 'error', + event: 'error', + }, + url: { + path: '/api/products/3', + scheme: 'http', + port: 3000, + domain: 'opbeans', + full: 'http://opbeans:3000/api/products/3', + }, + '@timestamp': '2021-10-19T13:57:02.546Z', + service: { + node: { + name: '59036ecb70908dfec4e03edc477f6875d08677871b4af0db3144373802d00cb1', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.11', + }, + language: { + name: 'Java', + version: '11.0.11', + }, + version: '2021-10-14 17:49:52', + }, + http: { + request: { + headers: { + 'User-Agent': ['http.rb/5.0.2'], + 'X-Forwarded-For': ['10.12.0.22, 10.12.0.14'], + Host: ['opbeans:3000'], + 'Accept-Encoding': ['gzip'], + 'Elastic-Apm-Traceparent': [ + '00-d5e80ae688f1fef91533f02dd2bdc769-35e3637e26919055-01', + ], + Tracestate: ['es=s:1.0'], + Traceparent: [ + '00-d5e80ae688f1fef91533f02dd2bdc769-35e3637e26919055-01', + ], + }, + method: 'GET', + }, + response: { + status_code: 500, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + transaction: { + id: '2c30263c4ad8fe8b', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: 'e7b69f99cb7523bedea6d7c97b684cf4b7ff458d0cba1efb1ac843300b3bf3c7', + }, + kubernetes: { + pod: { + uid: 'c5169b50-f3b3-4693-8e4b-150fca17c333', + name: 'opbeans-go-5d795ddf6b-rhlvf', + }, + }, + parent: { + id: '9f50f43e924d0b46', + }, + agent: { + name: 'go', + version: '1.14.0', + }, + process: { + args: [ + '/opbeans-go', + '-log-level=debug', + '-log-json', + '-listen=:3000', + '-frontend=/opbeans-frontend', + '-db=postgres:', + '-cache=redis://redis-master:6379', + ], + pid: 1, + title: 'opbeans-go', + ppid: 0, + }, + error: { + culprit: 'logrusMiddleware', + log: { + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'hook.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmlogrus@v1.14.0/hook.go', + line: { + number: 102, + }, + module: 'go.elastic.co/apm/module/apmlogrus', + function: '(*Hook).Fire', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'hooks.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/hooks.go', + line: { + number: 28, + }, + module: 'github.com/sirupsen/logrus', + function: 'LevelHooks.Fire', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + filename: 'entry.go', + line: { + number: 272, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).fireHooks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 241, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).log', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 293, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).Log', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 338, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).Logf', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 367, + }, + function: '(*Entry).Errorf', + module: 'github.com/sirupsen/logrus', + }, + { + exclude_from_grouping: false, + filename: 'logger.go', + abs_path: '/src/opbeans-go/logger.go', + line: { + number: 56, + }, + module: 'main', + function: 'logrusMiddleware', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + filename: 'context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmgin@v1.14.0/middleware.go', + filename: 'middleware.go', + line: { + number: 98, + }, + module: 'go.elastic.co/apm/module/apmgin', + function: '(*middleware).handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: + '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + function: '(*Context).Next', + module: 'github.com/gin-gonic/gin', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'cache.go', + abs_path: + '/go/pkg/mod/github.com/gin-contrib/cache@v1.1.0/cache.go', + line: { + number: 128, + }, + module: 'github.com/gin-contrib/cache', + function: 'Cache.func1', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: + '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'gin.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + line: { + number: 489, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Engine).handleHTTPRequest', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + filename: 'gin.go', + line: { + number: 445, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Engine).ServeHTTP', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'server.go', + abs_path: '/usr/local/go/src/net/http/server.go', + line: { + number: 2878, + }, + module: 'net/http', + function: 'serverHandler.ServeHTTP', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'server.go', + abs_path: '/usr/local/go/src/net/http/server.go', + line: { + number: 1929, + }, + module: 'net/http', + function: '(*conn).serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'asm_amd64.s', + abs_path: '/usr/local/go/src/runtime/asm_amd64.s', + line: { + number: 1581, + }, + module: 'runtime', + function: 'goexit', + }, + ], + level: 'error', + message: 'GET /api/products/3 (500)', + }, + id: '1660f82c1340f415e9a31b47565300ee', + grouping_key: '7a640436a9be648fd708703d1ac84650', + grouping_name: 'GET /api/products/3 (500)', + }, + message: 'GET /api/products/3 (500)', + processor: { + name: 'error', + event: 'error', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-lmf6c', + id: '7eedab18-1171-4a1b-a590-975e13fd103a', + ephemeral_id: '90034868-48e6-418c-8ab4-6616b403bca7', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.539Z', + ecs: { + version: '1.11.0', + }, + service: { + node: { + name: 'e7b69f99cb7523bedea6d7c97b684cf4b7ff458d0cba1efb1ac843300b3bf3c7', + }, + environment: 'testing', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.17.2', + }, + language: { + name: 'go', + version: 'go1.17.2', + }, + version: '2021-10-14 17:49:50', + }, + host: { + os: { + platform: 'linux', + }, + ip: '10.12.0.14', + architecture: 'amd64', + }, + event: { + ingested: '2021-10-19T13:57:05.412811279Z', + }, + transaction: { + id: '9f50f43e924d0b46', + }, + timestamp: { + us: 1634651822536408, + }, + }, + { + container: { + id: '015d1127421e2c3d42a0fb031fc75e989813f58973143b6c7e33dca6ccc6f31b', + }, + kubernetes: { + pod: { + uid: '459a6abf-198e-4107-b4dd-b0ae826755ab', + name: 'opbeans-go-nsn-69b89c4598-xsvgh', + }, + }, + parent: { + id: 'b7801be83bcdc972', + }, + process: { + args: [ + '/opbeans-go', + '-log-level=debug', + '-log-json', + '-listen=:3000', + '-frontend=/opbeans-frontend', + '-db=postgres:', + '-cache=redis://redis-master:6379', + ], + pid: 1, + title: 'opbeans-go', + ppid: 0, + }, + agent: { + name: 'go', + version: '1.14.0', + }, + message: 'GET /api/products/3 (500)', + error: { + culprit: 'logrusMiddleware', + log: { + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'hook.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmlogrus@v1.14.0/hook.go', + line: { + number: 102, + }, + module: 'go.elastic.co/apm/module/apmlogrus', + function: '(*Hook).Fire', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'hooks.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/hooks.go', + line: { + number: 28, + }, + function: 'LevelHooks.Fire', + module: 'github.com/sirupsen/logrus', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 272, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).fireHooks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 241, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).log', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 293, + }, + function: '(*Entry).Log', + module: 'github.com/sirupsen/logrus', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 338, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).Logf', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'entry.go', + abs_path: + '/go/pkg/mod/github.com/sirupsen/logrus@v1.8.1/entry.go', + line: { + number: 367, + }, + module: 'github.com/sirupsen/logrus', + function: '(*Entry).Errorf', + }, + { + exclude_from_grouping: false, + filename: 'logger.go', + abs_path: '/src/opbeans-go/logger.go', + line: { + number: 56, + }, + module: 'main', + function: 'logrusMiddleware', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: + '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'middleware.go', + abs_path: + '/go/pkg/mod/go.elastic.co/apm/module/apmgin@v1.14.0/middleware.go', + line: { + number: 98, + }, + module: 'go.elastic.co/apm/module/apmgin', + function: '(*middleware).handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: + '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'cache.go', + abs_path: + '/go/pkg/mod/github.com/gin-contrib/cache@v1.1.0/cache.go', + line: { + number: 128, + }, + module: 'github.com/gin-contrib/cache', + function: 'Cache.func1', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'context.go', + abs_path: + '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/context.go', + line: { + number: 165, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Context).Next', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + filename: 'gin.go', + line: { + number: 489, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Engine).handleHTTPRequest', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'gin.go', + abs_path: '/go/pkg/mod/github.com/gin-gonic/gin@v1.7.4/gin.go', + line: { + number: 445, + }, + module: 'github.com/gin-gonic/gin', + function: '(*Engine).ServeHTTP', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'server.go', + abs_path: '/usr/local/go/src/net/http/server.go', + line: { + number: 2878, + }, + module: 'net/http', + function: 'serverHandler.ServeHTTP', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'server.go', + abs_path: '/usr/local/go/src/net/http/server.go', + line: { + number: 1929, + }, + module: 'net/http', + function: '(*conn).serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'asm_amd64.s', + abs_path: '/usr/local/go/src/runtime/asm_amd64.s', + line: { + number: 1581, + }, + function: 'goexit', + module: 'runtime', + }, + ], + level: 'error', + message: 'GET /api/products/3 (500)', + }, + id: '7a265a869ad88851591e0e9734aa4a70', + grouping_key: '7a640436a9be648fd708703d1ac84650', + grouping_name: 'GET /api/products/3 (500)', + }, + processor: { + name: 'error', + event: 'error', + }, + cloud: { + availability_zone: 'us-central1-c', + instance: { + name: 'gke-dev-oblt-dev-oblt-pool-18e89389-qntq', + id: '5278603844673466232', + }, + provider: 'gcp', + machine: { + type: 'n1-standard-4', + }, + project: { + name: 'elastic-observability', + id: '8560181848', + }, + region: 'us-central1', + }, + observer: { + hostname: 'apm-apm-server-65d9d8dd68-zvs6p', + id: '69a7066f-46d2-42c4-a4cc-8400f60bf2b5', + ephemeral_id: '0ab88569-c301-40e9-8e78-cac7c1dac2bc', + type: 'apm-server', + version: '7.16.0', + version_major: 7, + }, + trace: { + id: 'd5e80ae688f1fef91533f02dd2bdc769', + }, + '@timestamp': '2021-10-19T13:57:02.539Z', + ecs: { + version: '1.11.0', + }, + service: { + node: { + name: '015d1127421e2c3d42a0fb031fc75e989813f58973143b6c7e33dca6ccc6f31b', + }, + environment: 'testing', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.17.2', + }, + language: { + name: 'go', + version: 'go1.17.2', + }, + version: '2021-10-14 17:49:50', + }, + host: { + os: { + platform: 'linux', + }, + ip: '10.12.0.13', + architecture: 'amd64', + }, + event: { + ingested: '2021-10-19T13:57:08.266888578Z', + }, + transaction: { + id: 'b7801be83bcdc972', + }, + timestamp: { + us: 1634651822536408, + }, + }, + ], +} as TraceAPIResponse; + +export const traceWithErrors = { + traceDocs: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 46, + }, + }, + source: { + ip: '172.19.0.13', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', + }, + language: { + name: 'Java', + version: '10.0.2', + }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13', + }, + body: { + original: '[REDACTED]', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + client: { + ip: '172.19.0.13', + }, + transaction: { + duration: { + us: 18842, + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other', + }, + }, + timestamp: { + us: 1584975868785000, + }, + }, + { + parent: { + id: 'fc107f7b556eb49b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + duration: { + us: 16597, + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + timestamp: { + us: 1584975868787052, + }, + }, + { + parent: { + id: 'daae24d83c269918', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + timestamp: { + us: 1584975868788603, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', + }, + language: { + name: 'python', + version: '3.6.10', + }, + version: 'None', + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648, + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1, + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + parent: { + id: '49809ad3c26adf74', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 44, + }, + }, + destination: { + address: 'opbeans-go', + port: 3000, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', + }, + language: { + name: 'Java', + version: '10.0.2', + }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + connection: { + hash: "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", + }, + transaction: { + id: '49809ad3c26adf74', + }, + timestamp: { + us: 1584975868785273, + }, + span: { + duration: { + us: 17530, + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external', + }, + }, + http: { + response: { + status_code: 200, + }, + url: { + original: 'http://opbeans-go:3000/api/orders', + }, + }, + id: 'fc107f7b556eb49b', + type: 'external', + }, + }, + { + parent: { + id: '975c8d5bfd1dd20b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + id: '975c8d5bfd1dd20b', + }, + timestamp: { + us: 1584975868787174, + }, + span: { + duration: { + us: 16250, + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external', + }, + }, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200, + }, + url: { + original: 'http://opbeans-python:3000/api/orders', + }, + }, + id: 'daae24d83c269918', + type: 'external', + }, + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', + }, + language: { + name: 'python', + version: '3.6.10', + }, + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + }, + timestamp: { + us: 1584975868790080, + }, + span: { + duration: { + us: 2519, + }, + subtype: 'postgresql', + name: 'SELECT "n"."id", "n"."address", "n"."city", "n"."company_name", "n"."country", "n"."email", "n"."full_name", "n"."postal_code" FROM "customers" AS "n" WHERE "n"."id" = @__id_0 LIMIT 1', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', + }, + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "n"."id", "n"."address", "n"."city", "n"."company_name", "n"."country", "n"."email", "n"."full_name", "n"."postal_code" FROM "customers" AS "n" WHERE "n"."id" = @__id_0 LIMIT 1', + type: 'sql', + }, + }, + }, + ], + exceedsMax: false, + errorDocs: [ + { + parent: { + id: '975c8d5bfd1dd20b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)', + }, + id: '1f3cb98206b5c54225cb7c8908a658da', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', + }, + processor: { + name: 'error', + event: 'error', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T16:04:28.787Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + id: '975c8d5bfd1dd20b', + sampled: false, + }, + timestamp: { + us: 1584975868787052, + }, + }, + { + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)', + }, + id: '1f3cb98206b5c54225cb7c8908a658d2', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', + }, + processor: { + name: 'error', + event: 'error', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T16:04:28.790Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-python', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + sampled: false, + }, + timestamp: { + us: 1584975868790000, + }, + }, + ], +} as unknown as TraceAPIResponse; + +export const traceChildStartBeforeParent = { + traceDocs: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 46, + }, + }, + source: { + ip: '172.19.0.13', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', + }, + language: { + name: 'Java', + version: '10.0.2', + }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13', + }, + body: { + original: '[REDACTED]', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + client: { + ip: '172.19.0.13', + }, + transaction: { + duration: { + us: 18842, + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other', + }, + }, + timestamp: { + us: 1584975868785000, + }, + }, + { + parent: { + id: 'fc107f7b556eb49b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + duration: { + us: 16597, + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + timestamp: { + us: 1584975868787052, + }, + }, + { + parent: { + id: 'daae24d83c269918', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + timestamp: { + us: 1584975868780000, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', + }, + language: { + name: 'python', + version: '3.6.10', + }, + version: 'None', + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 1464, + }, + name: 'I started before my parent 😰', + span_count: { + dropped: 0, + started: 1, + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + parent: { + id: '49809ad3c26adf74', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 44, + }, + }, + destination: { + address: 'opbeans-go', + port: 3000, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', + }, + language: { + name: 'Java', + version: '10.0.2', + }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + connection: { + hash: "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", + }, + transaction: { + id: '49809ad3c26adf74', + }, + timestamp: { + us: 1584975868785273, + }, + span: { + duration: { + us: 17530, + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external', + }, + }, + http: { + response: { + status_code: 200, + }, + url: { + original: 'http://opbeans-go:3000/api/orders', + }, + }, + id: 'fc107f7b556eb49b', + type: 'external', + }, + }, + { + parent: { + id: '975c8d5bfd1dd20b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + id: '975c8d5bfd1dd20b', + }, + timestamp: { + us: 1584975868787174, + }, + span: { + duration: { + us: 16250, + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external', + }, + }, + name: 'I am his 👇🏻 parent 😡', + http: { + response: { + status_code: 200, + }, + url: { + original: 'http://opbeans-python:3000/api/orders', + }, + }, + id: 'daae24d83c269918', + type: 'external', + }, + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', + }, + language: { + name: 'python', + version: '3.6.10', + }, + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + }, + timestamp: { + us: 1584975868781000, + }, + span: { + duration: { + us: 2519, + }, + subtype: 'postgresql', + name: 'I am using my parents skew 😇', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', + }, + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql', + }, + }, + }, + ], + exceedsMax: false, + errorDocs: [], +} as TraceAPIResponse; + +export const inferredSpans = { + traceDocs: [ + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + source: { + ip: '172.18.0.8', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/products/2', + scheme: 'http', + port: 3000, + domain: '172.18.0.7', + full: 'http://172.18.0.7:3000/api/products/2', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.786Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', + }, + language: { + name: 'Java', + version: '11.0.6', + }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + client: { + ip: '172.18.0.8', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.18.0.7:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.18.0.8', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Thu, 09 Apr 2020 11:36:01 GMT'], + 'Content-Type': ['application/json;charset=UTF-8'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other', + }, + }, + transaction: { + duration: { + us: 237537, + }, + result: 'HTTP 2xx', + name: 'APIRestController#product', + span_count: { + dropped: 0, + started: 3, + }, + id: 'f2387d37260d00bd', + type: 'request', + sampled: true, + }, + timestamp: { + us: 1586432160786001, + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: 'f2387d37260d00bd', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.810Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', + }, + language: { + name: 'Java', + version: '11.0.6', + }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + span: { + duration: { + us: 204574, + }, + subtype: 'inferred', + name: 'ServletInvocableHandlerMethod#invokeAndHandle', + id: 'a5df600bd7bd5e38', + type: 'app', + }, + timestamp: { + us: 1586432160810441, + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: 'a5df600bd7bd5e38', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + type: 'apm-server', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.810Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', + }, + language: { + name: 'Java', + version: '11.0.6', + }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160810441, + }, + span: { + duration: { + us: 102993, + }, + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'InvocableHandlerMethod.java', + line: { + number: -1, + }, + function: 'doInvoke', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'InvocableHandlerMethod.java', + line: { + number: -1, + }, + function: 'invokeForRequest', + }, + ], + subtype: 'inferred', + name: 'APIRestController#product', + id: '808dc34fc41ce522', + type: 'app', + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: 'f2387d37260d00bd', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'span', + }, + labels: { + productId: '2', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.832Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', + }, + language: { + name: 'Java', + version: '11.0.6', + }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160832300, + }, + span: { + duration: { + us: 99295, + }, + name: 'OpenTracing product span', + id: '41226ae63af4f235', + type: 'unknown', + }, + child: { id: ['8d80de06aa11a6fc'] }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: '808dc34fc41ce522', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.859Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', + }, + language: { + name: 'Java', + version: '11.0.6', + }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160859600, + }, + span: { + duration: { + us: 53835, + }, + subtype: 'inferred', + name: 'Loader#executeQueryStatement', + id: '8d80de06aa11a6fc', + type: 'app', + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: '41226ae63af4f235', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + destination: { + address: 'postgres', + port: 5432, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.903Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', + }, + language: { + name: 'Java', + version: '11.0.6', + }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160903236, + }, + span: { + duration: { + us: 10211, + }, + subtype: 'postgresql', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', + }, + }, + name: 'SELECT FROM products', + action: 'query', + id: '3708d5623658182f', + type: 'db', + db: { + statement: + 'select product0_.id as col_0_0_, product0_.sku as col_1_0_, product0_.name as col_2_0_, product0_.description as col_3_0_, product0_.cost as col_4_0_, product0_.selling_price as col_5_0_, product0_.stock as col_6_0_, producttyp1_.id as col_7_0_, producttyp1_.name as col_8_0_, (select sum(orderline2_.amount) from order_lines orderline2_ where orderline2_.product_id=product0_.id) as col_9_0_ from products product0_ left outer join product_types producttyp1_ on product0_.type_id=producttyp1_.id where product0_.id=?', + type: 'sql', + user: { + name: 'postgres', + }, + }, + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: '41226ae63af4f235', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + destination: { + address: 'postgres', + port: 5432, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.859Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', + }, + language: { + name: 'Java', + version: '11.0.6', + }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160859508, + }, + span: { + duration: { + us: 4503, + }, + subtype: 'postgresql', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', + }, + }, + name: 'empty query', + action: 'query', + id: '9871cfd612368932', + type: 'db', + db: { + rows_affected: 0, + statement: '(empty query)', + type: 'sql', + user: { + name: 'postgres', + }, + }, + }, + }, + ], + exceedsMax: false, + errorDocs: [], +} as TraceAPIResponse; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx new file mode 100644 index 0000000000000..895b83136a097 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Meta, Story } from '@storybook/react'; +import React, { ComponentProps } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; +import { WaterfallContainer } from './index'; +import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; +import { + inferredSpans, + manyChildrenWithSameLength, + simpleTrace, + traceChildStartBeforeParent, + traceWithErrors, + urlParams as testUrlParams, +} from './waterfall_container.stories.data'; + +type Args = ComponentProps; + +const stories: Meta = { + title: 'app/TransactionDetails/Waterfall', + component: WaterfallContainer, + decorators: [ + (StoryComponent) => ( + + + + + + ), + ], +}; +export default stories; + +export const Example: Story = ({ urlParams, waterfall }) => { + return ; +}; +Example.args = { + urlParams: testUrlParams, + waterfall: getWaterfall(simpleTrace, '975c8d5bfd1dd20b'), +}; + +export const WithErrors: Story = ({ urlParams, waterfall }) => { + return ; +}; +WithErrors.args = { + urlParams: testUrlParams, + waterfall: getWaterfall(traceWithErrors, '975c8d5bfd1dd20b'), +}; + +export const ChildStartsBeforeParent: Story = ({ + urlParams, + waterfall, +}) => { + return ; +}; +ChildStartsBeforeParent.args = { + urlParams: testUrlParams, + waterfall: getWaterfall(traceChildStartBeforeParent, '975c8d5bfd1dd20b'), +}; + +export const InferredSpans: Story = ({ urlParams, waterfall }) => { + return ; +}; +InferredSpans.args = { + urlParams: testUrlParams, + waterfall: getWaterfall(inferredSpans, 'f2387d37260d00bd'), +}; + +export const ManyChildrenWithSameLength: Story = ({ + urlParams, + waterfall, +}) => { + return ; +}; +ManyChildrenWithSameLength.args = { + urlParams: testUrlParams, + waterfall: getWaterfall(manyChildrenWithSameLength, '9a7f717439921d39'), +}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.test.tsx new file mode 100644 index 0000000000000..47610569dfa06 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { composeStories } from '@storybook/testing-react'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { disableConsoleWarning } from '../../../../../utils/testHelpers'; +import * as stories from './waterfall_container.stories'; + +const { Example } = composeStories(stories); + +describe('WaterfallContainer', () => { + let consoleMock: jest.SpyInstance; + + beforeAll(() => { + consoleMock = disableConsoleWarning('Warning: componentWillReceiveProps'); + }); + + afterAll(() => { + consoleMock.mockRestore(); + }); + + it('renders', () => { + expect(() => render()).not.toThrowError(); + }); + + it('expands and contracts the accordion', () => { + const { getAllByRole } = render(); + const buttons = getAllByRole('button'); + const parentItem = buttons[2]; + const childItem = buttons[3]; + + parentItem.click(); + + expect(parentItem).toHaveAttribute('aria-expanded', 'false'); + expect(childItem).toHaveAttribute('aria-expanded', 'true'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index bc60c917094f8..f84668068f413 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -32,7 +32,8 @@ export const APP_SEARCH_PLUGIN = { 'Leverage dashboards, analytics, and APIs for advanced application search made simple.', }), CARD_DESCRIPTION: i18n.translate('xpack.enterpriseSearch.appSearch.productCardDescription', { - defaultMessage: 'Design and deploy a powerful search to your websites and apps.', + defaultMessage: + 'Design, deploy, and manage powerful search experiences for your websites and web/mobile apps.', }), URL: '/app/enterprise_search/app_search', SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/app-search/', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index a43fb6f293457..a0df5337b2e2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -288,6 +288,9 @@ export const SOURCE_OBJ_TYPES = { CAMPAIGNS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.campaigns', { defaultMessage: 'Campaigns', }), + CASES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.cases', { + defaultMessage: 'Cases (including feeds and comments)', + }), USERS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.users', { defaultMessage: 'Users', }), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index c303190651f57..244ce79135cab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -432,6 +432,7 @@ export const staticSourceData = [ SOURCE_OBJ_TYPES.LEADS, SOURCE_OBJ_TYPES.ACCOUNTS, SOURCE_OBJ_TYPES.CAMPAIGNS, + SOURCE_OBJ_TYPES.CASES, ], features: { basicOrgContext: [ @@ -466,6 +467,7 @@ export const staticSourceData = [ SOURCE_OBJ_TYPES.LEADS, SOURCE_OBJ_TYPES.ACCOUNTS, SOURCE_OBJ_TYPES.CAMPAIGNS, + SOURCE_OBJ_TYPES.CASES, ], features: { basicOrgContext: [ diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 772a8cbe7ec35..9f395cb0d94bc 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -18,6 +18,7 @@ export { } from './services/exception_lists/exception_list_client_types'; export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export type { ListPluginSetup, ListsApiRequestHandlerContext } from './types'; +export type { ExportExceptionListAndItemsReturn } from './services/exception_lists/export_exception_list_and_items'; export const config: PluginConfigDescriptor = { schema: ConfigSchema, diff --git a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts index aa30c8a7d435d..b91537b6cb3b1 100644 --- a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts @@ -6,7 +6,6 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; -import { transformDataToNdjson } from '@kbn/securitysolution-utils'; import { exportExceptionListQuerySchema } from '@kbn/securitysolution-io-ts-list-types'; import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; @@ -30,43 +29,28 @@ export const exportExceptionListRoute = (router: ListsPluginRouter): void => { try { const { id, list_id: listId, namespace_type: namespaceType } = request.query; - const exceptionLists = getExceptionListClient(context); - const exceptionList = await exceptionLists.getExceptionList({ + const exceptionListsClient = getExceptionListClient(context); + + const exportContent = await exceptionListsClient.exportExceptionListAndItems({ id, listId, namespaceType, }); - if (exceptionList == null) { + if (exportContent == null) { return siemResponse.error({ - body: `exception list with list_id: ${listId} does not exist`, + body: `exception list with list_id: ${listId} or id: ${id} does not exist`, statusCode: 400, }); - } else { - const listItems = await exceptionLists.findExceptionListItem({ - filter: undefined, - listId, - namespaceType, - page: 1, - perPage: 10000, - sortField: 'exception-list.created_at', - sortOrder: 'desc', - }); - const exceptionItems = listItems?.data ?? []; - - const { exportData } = getExport([exceptionList, ...exceptionItems]); - const { exportDetails } = getExportDetails(exceptionItems); - - // TODO: Allow the API to override the name of the file to export - const fileName = exceptionList.list_id; - return response.ok({ - body: `${exportData}${exportDetails}`, - headers: { - 'Content-Disposition': `attachment; filename="${fileName}"`, - 'Content-Type': 'application/ndjson', - }, - }); } + + return response.ok({ + body: `${exportContent.exportData}${JSON.stringify(exportContent.exportDetails)}\n`, + headers: { + 'Content-Disposition': `attachment; filename="${listId}"`, + 'Content-Type': 'application/ndjson', + }, + }); } catch (err) { const error = transformError(err); return siemResponse.error({ @@ -77,24 +61,3 @@ export const exportExceptionListRoute = (router: ListsPluginRouter): void => { } ); }; - -export const getExport = ( - data: unknown[] -): { - exportData: string; -} => { - const ndjson = transformDataToNdjson(data); - - return { exportData: ndjson }; -}; - -export const getExportDetails = ( - items: unknown[] -): { - exportDetails: string; -} => { - const exportDetails = JSON.stringify({ - exported_list_items_count: items.length, - }); - return { exportDetails: `${exportDetails}\n` }; -}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts index 1566241e7351e..f5f6a4f1f2d5a 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts @@ -30,6 +30,17 @@ export class ExceptionListClientMock extends ExceptionListClient { public findExceptionList = jest.fn().mockResolvedValue(getFoundExceptionListSchemaMock()); public createTrustedAppsList = jest.fn().mockResolvedValue(getTrustedAppsListSchemaMock()); public createEndpointList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); + public exportExceptionListAndItems = jest.fn().mockResolvedValue({ + exportData: 'exportString', + exportDetails: { + exported_exception_list_count: 0, + exported_exception_list_item_count: 0, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, + }, + }); } export const getExceptionListClientMock = ( diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index 77e82bf0f7578..542598fc82c90 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -24,6 +24,7 @@ import { DeleteExceptionListItemByIdOptions, DeleteExceptionListItemOptions, DeleteExceptionListOptions, + ExportExceptionListAndItemsOptions, FindEndpointListItemOptions, FindExceptionListItemOptions, FindExceptionListOptions, @@ -38,6 +39,10 @@ import { UpdateExceptionListOptions, } from './exception_list_client_types'; import { getExceptionList } from './get_exception_list'; +import { + ExportExceptionListAndItemsReturn, + exportExceptionListAndItems, +} from './export_exception_list_and_items'; import { getExceptionListSummary } from './get_exception_list_summary'; import { createExceptionList } from './create_exception_list'; import { getExceptionListItem } from './get_exception_list_item'; @@ -492,4 +497,19 @@ export class ExceptionListClient { sortOrder, }); }; + + public exportExceptionListAndItems = async ({ + listId, + id, + namespaceType, + }: ExportExceptionListAndItemsOptions): Promise => { + const { savedObjectsClient } = this; + + return exportExceptionListAndItems({ + id, + listId, + namespaceType, + savedObjectsClient, + }); + }; } diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index b734d3a7b1a3b..14de474974c11 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -220,3 +220,21 @@ export interface FindExceptionListOptions { sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; } + +export interface ExportExceptionListAndItemsOptions { + listId: ListIdOrUndefined; + id: IdOrUndefined; + namespaceType: NamespaceType; +} + +export interface ExportExceptionListAndItemsReturn { + exportData: string; + exportDetails: { + exported_exception_list_count: number; + exported_exception_list_item_count: number; + missing_exception_list_item_count: number; + missing_exception_list_items: string[]; + missing_exception_lists: string[]; + missing_exception_lists_count: number; + }; +} diff --git a/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.test.ts b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.test.ts new file mode 100644 index 0000000000000..9f3c02fecca20 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; + +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; +import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock'; + +import { exportExceptionListAndItems } from './export_exception_list_and_items'; +import { findExceptionListItem } from './find_exception_list_item'; +import { getExceptionList } from './get_exception_list'; + +jest.mock('./get_exception_list'); +jest.mock('./find_exception_list_item'); + +describe('export_exception_list_and_items', () => { + describe('exportExceptionListAndItems', () => { + test('it should return null if no matching exception list found', async () => { + (getExceptionList as jest.Mock).mockResolvedValue(null); + (findExceptionListItem as jest.Mock).mockResolvedValue({ data: [] }); + + const result = await exportExceptionListAndItems({ + id: '123', + listId: 'non-existent', + namespaceType: 'single', + savedObjectsClient: {} as SavedObjectsClientContract, + }); + expect(result).toBeNull(); + }); + + test('it should return stringified list and items', async () => { + (getExceptionList as jest.Mock).mockResolvedValue(getExceptionListSchemaMock()); + (findExceptionListItem as jest.Mock).mockResolvedValue({ + data: [getExceptionListItemSchemaMock()], + }); + + const result = await exportExceptionListAndItems({ + id: '123', + listId: 'non-existent', + namespaceType: 'single', + savedObjectsClient: {} as SavedObjectsClientContract, + }); + expect(result?.exportData).toEqual( + `${JSON.stringify(getExceptionListSchemaMock())}\n${JSON.stringify( + getExceptionListItemSchemaMock() + )}\n` + ); + expect(result?.exportDetails).toEqual({ + exported_exception_list_count: 1, + exported_exception_list_item_count: 1, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, + }); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.ts b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.ts new file mode 100644 index 0000000000000..46b3df4e5ac44 --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/export_exception_list_and_items.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + IdOrUndefined, + ListIdOrUndefined, + NamespaceType, +} from '@kbn/securitysolution-io-ts-list-types'; +import { transformDataToNdjson } from '@kbn/securitysolution-utils'; +import { SavedObjectsClientContract } from 'kibana/server'; + +import { findExceptionListItem } from './find_exception_list_item'; +import { getExceptionList } from './get_exception_list'; + +interface ExportExceptionListAndItemsOptions { + id: IdOrUndefined; + listId: ListIdOrUndefined; + savedObjectsClient: SavedObjectsClientContract; + namespaceType: NamespaceType; +} + +export interface ExportExceptionListAndItemsReturn { + exportData: string; + exportDetails: { + exported_exception_list_count: number; + exported_exception_list_item_count: number; + missing_exception_list_item_count: number; + missing_exception_list_items: string[]; + missing_exception_lists: string[]; + missing_exception_lists_count: number; + }; +} + +export const exportExceptionListAndItems = async ({ + id, + listId, + namespaceType, + savedObjectsClient, +}: ExportExceptionListAndItemsOptions): Promise => { + const exceptionList = await getExceptionList({ + id, + listId, + namespaceType, + savedObjectsClient, + }); + + if (exceptionList == null) { + return null; + } else { + // TODO: Will need to address this when we switch over to + // using PIT, don't want it to get lost + // https://github.com/elastic/kibana/issues/103944 + const listItems = await findExceptionListItem({ + filter: undefined, + listId: exceptionList.list_id, + namespaceType: exceptionList.namespace_type, + page: 1, + perPage: 10000, + savedObjectsClient, + sortField: 'exception-list.created_at', + sortOrder: 'desc', + }); + const exceptionItems = listItems?.data ?? []; + const { exportData } = getExport([exceptionList, ...exceptionItems]); + + // TODO: Add logic for missing lists and items on errors + return { + exportData: `${exportData}`, + exportDetails: { + exported_exception_list_count: 1, + exported_exception_list_item_count: exceptionItems.length, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, + }, + }; + } +}; + +export const getExport = ( + data: unknown[] +): { + exportData: string; +} => { + const ndjson = transformDataToNdjson(data); + + return { exportData: ndjson }; +}; diff --git a/x-pack/plugins/lists/server/services/exception_lists/index.ts b/x-pack/plugins/lists/server/services/exception_lists/index.ts index e6a6dd7ef8c3c..fbc052936931a 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/index.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/index.ts @@ -10,6 +10,7 @@ export * from './create_exception_list_item'; export * from './delete_exception_list'; export * from './delete_exception_list_item'; export * from './delete_exception_list_items_by_list'; +export * from './export_exception_list_and_items'; export * from './find_exception_list'; export * from './find_exception_list_item'; export * from './find_exception_list_items'; diff --git a/x-pack/plugins/maps/server/routes.js b/x-pack/plugins/maps/server/routes.js index a25a28c4da21c..162c305a69ca5 100644 --- a/x-pack/plugins/maps/server/routes.js +++ b/x-pack/plugins/maps/server/routes.js @@ -488,10 +488,11 @@ export async function initRoutes(core, getLicenseId, emsSettings, kbnVersion, lo }, (context, request, response) => { const range = path.normalize(request.params.range); - return range.startsWith('..') + const rootPath = path.resolve(__dirname, 'fonts', 'open_sans'); + const fontPath = path.resolve(rootPath, `${range}.pbf`); + return !fontPath.startsWith(rootPath) ? response.notFound() : new Promise((resolve) => { - const fontPath = path.join(__dirname, 'fonts', 'open_sans', `${range}.pbf`); fs.readFile(fontPath, (error, data) => { if (error) { resolve(response.notFound()); diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx index fedb07fa65a40..2543b054ee5bb 100644 --- a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx @@ -15,7 +15,7 @@ import { useTable } from '../../hooks/use_table'; import { ApmTemplate } from './apm_template'; // @ts-ignore import { ApmServerInstances } from '../../../components/apm/instances'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { APM_SYSTEM_ID } from '../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx index 4611f17159621..b33789f510f2e 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx @@ -15,7 +15,7 @@ import { useTable } from '../../hooks/use_table'; import { BeatsTemplate } from './beats_template'; // @ts-ignore import { Listing } from '../../../components/beats/listing'; -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { BEATS_SYSTEM_ID } from '../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index 04074762c8d22..2ffbc3a75ce05 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -14,7 +14,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context'; import { TabMenuItem } from '../page_template'; import { Overview } from '../../../components/cluster/overview'; import { ExternalConfigContext } from '../../contexts/external_config_context'; -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { fetchClusters } from '../../../lib/fetch_clusters'; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx index cb37705c959aa..2ab18331d1cdb 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx @@ -13,7 +13,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore import { Ccr } from '../../../components/elasticsearch/ccr'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx index 29cf9ade8d997..2ded26df16323 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx @@ -15,7 +15,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore import { CcrShardReact } from '../../../components/elasticsearch/ccr_shard'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx index f2f2ec36b7cd9..c51027636b287 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx @@ -11,7 +11,7 @@ import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useCharts } from '../../hooks/use_charts'; import { ItemTemplate } from './item_template'; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx index 8e70a99e67914..422f051c7d718 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx @@ -13,7 +13,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore import { IndexReact } from '../../../components/elasticsearch/index/index_react'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useCharts } from '../../hooks/use_charts'; import { ItemTemplate } from './item_template'; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx index 277bde2ac35cb..6618db7eebe66 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { GlobalStateContext } from '../../contexts/global_state_context'; import { ElasticsearchIndices } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; import { useLocalStorage } from '../../hooks/use_local_storage'; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx index b97007f1c1462..46bb4cc20242f 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx @@ -12,7 +12,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { GlobalStateContext } from '../../contexts/global_state_context'; import { ElasticsearchMLJobs } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; import type { MLJobs } from '../../../types'; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx index b2d6fb94183ec..a75c8447a3561 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx @@ -13,7 +13,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { GlobalStateContext } from '../../contexts/global_state_context'; import { NodeReact } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useLocalStorage } from '../../hooks/use_local_storage'; import { useCharts } from '../../hooks/use_charts'; 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 index ac7e267cbc9ac..9933188b887d5 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/nodes_page.tsx @@ -13,7 +13,7 @@ import { GlobalStateContext } from '../../contexts/global_state_context'; import { ExternalConfigContext } from '../../contexts/external_config_context'; import { ElasticsearchNodes } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx index 076e9413216fb..a27c1418eabc1 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx @@ -16,7 +16,7 @@ import { KibanaTemplate } from './kibana_template'; // @ts-ignore import { KibanaInstances } from '../../../components/kibana/instances'; // @ts-ignore -import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer, SetupModeProps } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { AlertsByName } from '../../../alerts/types'; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx index 0fd10a93bcd83..447a7b1792fb9 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx @@ -13,7 +13,7 @@ import { ComponentProps } from '../../route_init'; // @ts-ignore import { Listing } from '../../../components/logstash/listing'; import { LogstashTemplate } from './logstash_template'; -import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; +import { SetupModeRenderer } from '../../../components/renderers/setup_mode'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; import { LOGSTASH_SYSTEM_ID, RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx index 52780aa280707..92def5b2c36f2 100644 --- a/x-pack/plugins/monitoring/public/application/route_init.tsx +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -9,7 +9,7 @@ import { Route, Redirect, useLocation } from 'react-router-dom'; import { useClusters } from './hooks/use_clusters'; import { GlobalStateContext } from './contexts/global_state_context'; import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; -import { isInSetupMode } from './setup_mode'; +import { isInSetupMode } from '../lib/setup_mode'; import { LoadingPage } from './pages/loading_page'; export interface ComponentProps { diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js deleted file mode 100644 index df524fa99ae53..0000000000000 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { - getSetupModeState, - initSetupModeState, - updateSetupModeData, - disableElasticsearchInternalCollection, - toggleSetupMode, - setSetupModeMenuItem, -} from '../../lib/setup_mode'; -import { Flyout } from '../../components/metricbeat_migration/flyout'; -import { - EuiBottomBar, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiTextColor, - EuiIcon, - EuiSpacer, -} from '@elastic/eui'; -import { findNewUuid } from '../../components/renderers/lib/find_new_uuid'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { GlobalStateContext } from '../../application/contexts/global_state_context'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { useRequestErrorHandler } from '../hooks/use_request_error_handler'; - -class WrappedSetupModeRenderer extends React.Component { - globalState; - state = { - renderState: false, - isFlyoutOpen: false, - instance: null, - newProduct: null, - isSettingUpNew: false, - }; - - UNSAFE_componentWillMount() { - this.globalState = this.context; - const { kibana, onHttpError } = this.props; - initSetupModeState(this.globalState, kibana.services.http, onHttpError, (_oldData) => { - const newState = { renderState: true }; - - const { productName } = this.props; - if (!productName) { - this.setState(newState); - return; - } - - const setupModeState = getSetupModeState(); - if (!setupModeState.enabled || !setupModeState.data) { - this.setState(newState); - return; - } - - const data = setupModeState.data[productName]; - const oldData = _oldData ? _oldData[productName] : null; - if (data && oldData) { - const newUuid = findNewUuid(Object.keys(oldData.byUuid), Object.keys(data.byUuid)); - if (newUuid) { - newState.newProduct = data.byUuid[newUuid]; - } - } - - this.setState(newState); - }); - setSetupModeMenuItem(); - } - - reset() { - this.setState({ - renderState: false, - isFlyoutOpen: false, - instance: null, - newProduct: null, - isSettingUpNew: false, - }); - } - - getFlyout(data, meta) { - const { productName } = this.props; - const { isFlyoutOpen, instance, isSettingUpNew, newProduct } = this.state; - if (!data || !isFlyoutOpen) { - return null; - } - - let product = null; - if (newProduct) { - product = newProduct; - } - // For new instance discovery flow, we pass in empty instance object - else if (instance && Object.keys(instance).length) { - product = data.byUuid[instance.uuid]; - } - - if (!product) { - const uuids = Object.values(data.byUuid); - if (uuids.length && !isSettingUpNew) { - product = uuids[0]; - } else { - product = { - isNetNewUser: true, - }; - } - } - - return ( - this.reset()} - productName={productName} - product={product} - meta={meta} - instance={instance} - updateProduct={updateSetupModeData} - isSettingUpNew={isSettingUpNew} - /> - ); - } - - getBottomBar(setupModeState) { - if (!setupModeState.enabled || setupModeState.hideBottomBar) { - return null; - } - - return ( - - - - - - - - - , - }} - /> - - - - - - - - toggleSetupMode(false)} - > - {i18n.translate('xpack.monitoring.setupMode.exit', { - defaultMessage: `Exit setup mode`, - })} - - - - - - - - ); - } - - async shortcutToFinishMigration() { - await disableElasticsearchInternalCollection(); - await updateSetupModeData(); - } - - render() { - const { render, productName } = this.props; - const setupModeState = getSetupModeState(); - - let data = { byUuid: {} }; - if (setupModeState.data) { - if (productName && setupModeState.data[productName]) { - data = setupModeState.data[productName]; - } else if (setupModeState.data) { - data = setupModeState.data; - } - } - - const meta = setupModeState.data ? setupModeState.data._meta : null; - - return render({ - setupMode: { - data, - meta, - enabled: setupModeState.enabled, - productName, - updateSetupModeData, - shortcutToFinishMigration: () => this.shortcutToFinishMigration(), - openFlyout: (instance, isSettingUpNew) => - this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }), - closeFlyout: () => this.setState({ isFlyoutOpen: false }), - }, - flyoutComponent: this.getFlyout(data, meta), - bottomBarComponent: this.getBottomBar(setupModeState), - }); - } -} - -function withErrorHandler(Component) { - return function WrappedComponent(props) { - const handleRequestError = useRequestErrorHandler(); - return ; - }; -} - -WrappedSetupModeRenderer.contextType = GlobalStateContext; -export const SetupModeRenderer = withKibana(withErrorHandler(WrappedSetupModeRenderer)); diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.d.ts similarity index 100% rename from x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts rename to x-pack/plugins/monitoring/public/components/renderers/setup_mode.d.ts diff --git a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js index 573f7e7e33c5e..cfa57559d2bc9 100644 --- a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js @@ -27,8 +27,12 @@ import { import { findNewUuid } from './lib/find_new_uuid'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { GlobalStateContext } from '../../application/contexts/global_state_context'; +import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useRequestErrorHandler } from '../../application/hooks/use_request_error_handler'; -export class SetupModeRenderer extends React.Component { +export class WrappedSetupModeRenderer extends React.Component { + globalState; state = { renderState: false, isFlyoutOpen: false, @@ -38,9 +42,11 @@ export class SetupModeRenderer extends React.Component { }; UNSAFE_componentWillMount() { - const { scope, injector } = this.props; - initSetupModeState(scope, injector, (_oldData) => { + this.globalState = this.context; + const { kibana, onHttpError } = this.props; + initSetupModeState(this.globalState, kibana.services.http, onHttpError, (_oldData) => { const newState = { renderState: true }; + const { productName } = this.props; if (!productName) { this.setState(newState); @@ -207,3 +213,13 @@ export class SetupModeRenderer extends React.Component { }); } } + +function withErrorHandler(Component) { + return function WrappedComponent(props) { + const handleRequestError = useRequestErrorHandler(); + return ; + }; +} + +WrappedSetupModeRenderer.contextType = GlobalStateContext; +export const SetupModeRenderer = withKibana(withErrorHandler(WrappedSetupModeRenderer)); diff --git a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js index cb1593d084366..9672da8b8088a 100644 --- a/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js +++ b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js @@ -9,6 +9,14 @@ import React, { Fragment } from 'react'; import { shallow } from 'enzyme'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../common/constants'; +const kibanaMock = { + services: { + http: jest.fn(), + }, +}; + +const onHttpErrorMock = jest.fn(); + describe('SetupModeRenderer', () => { beforeEach(() => jest.resetModules()); @@ -21,16 +29,14 @@ describe('SetupModeRenderer', () => { updateSetupModeData: () => {}, setSetupModeMenuItem: () => {}, })); - const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer; const ChildComponent = () =>

Hi

; - const scope = {}; - const injector = {}; const component = shallow( ( {flyoutComponent} @@ -57,16 +63,14 @@ describe('SetupModeRenderer', () => { updateSetupModeData: () => {}, setSetupModeMenuItem: () => {}, })); - const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer; const ChildComponent = () =>

Hi

; - const scope = {}; - const injector = {}; const component = shallow( ( {flyoutComponent} @@ -95,16 +99,14 @@ describe('SetupModeRenderer', () => { updateSetupModeData: () => {}, setSetupModeMenuItem: () => {}, })); - const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer; const ChildComponent = () =>

Hi

; - const scope = {}; - const injector = {}; const component = shallow( ( {flyoutComponent} @@ -135,16 +137,14 @@ describe('SetupModeRenderer', () => { updateSetupModeData: () => {}, setSetupModeMenuItem: () => {}, })); - const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer; const ChildComponent = () =>

Hi

; - const scope = {}; - const injector = {}; const component = shallow( ( {flyoutComponent} @@ -176,7 +176,7 @@ describe('SetupModeRenderer', () => { _meta: {}, }, }), - initSetupModeState: (_scope, _injectir, cb) => { + initSetupModeState: (_globalState, _httpService, _onError, cb) => { setTimeout(() => { cb({ elasticsearch: { @@ -190,16 +190,14 @@ describe('SetupModeRenderer', () => { updateSetupModeData: () => {}, setSetupModeMenuItem: () => {}, })); - const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer; const ChildComponent = () =>

Hi

; - const scope = {}; - const injector = {}; const component = shallow( ( {flyoutComponent} @@ -235,7 +233,7 @@ describe('SetupModeRenderer', () => { _meta: {}, }, }), - initSetupModeState: (_scope, _injectir, cb) => { + initSetupModeState: (_globalState, _httpService, _onError, cb) => { setTimeout(() => { cb({ elasticsearch: { @@ -249,16 +247,14 @@ describe('SetupModeRenderer', () => { updateSetupModeData: () => {}, setSetupModeMenuItem, })); - const SetupModeRenderer = require('./setup_mode').SetupModeRenderer; + const SetupModeRenderer = require('./setup_mode').WrappedSetupModeRenderer; const ChildComponent = () =>

Hi

; - const scope = {}; - const injector = {}; const component = shallow( ( {flyoutComponent} diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js index 47cae9c4f0851..bacc305764d68 100644 --- a/x-pack/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js @@ -11,11 +11,8 @@ let getSetupModeState; let updateSetupModeData; let setSetupModeMenuItem; -jest.mock('./ajax_error_handler', () => ({ - ajaxErrorHandlersProvider: (err) => { - throw err; - }, -})); +const handleErrorsMock = jest.fn(); +const callbackMock = jest.fn(); jest.mock('react-dom', () => ({ render: jest.fn(), @@ -25,7 +22,6 @@ jest.mock('../legacy_shims', () => { return { Legacy: { shims: { - getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }), toastNotifications: { addDanger: jest.fn(), }, @@ -35,41 +31,8 @@ jest.mock('../legacy_shims', () => { }; }); -let data = {}; - -const injectorModulesMock = { - globalState: { - save: jest.fn(), - }, - Private: (module) => module, - $http: { - post: jest.fn().mockImplementation(() => { - return { data }; - }), - }, - $executor: { - run: jest.fn(), - }, -}; - -const angularStateMock = { - injector: { - get: (module) => { - return injectorModulesMock[module] || {}; - }, - }, - scope: { - $apply: (fn) => fn && fn(), - $evalAsync: (fn) => fn && fn(), - }, -}; - -// We are no longer waiting for setup mode data to be fetched when enabling -// so we need to wait for the next tick for the async action to finish - function setModulesAndMocks() { jest.clearAllMocks().resetModules(); - injectorModulesMock.globalState.inSetupMode = false; const setupMode = require('./setup_mode'); toggleSetupMode = setupMode.toggleSetupMode; @@ -83,53 +46,76 @@ function waitForSetupModeData() { return new Promise((resolve) => process.nextTick(resolve)); } -xdescribe('setup_mode', () => { +describe('setup_mode', () => { beforeEach(async () => { setModulesAndMocks(); }); describe('setup', () => { - it('should require angular state', async () => { - let error; - try { - toggleSetupMode(true); - } catch (err) { - error = err; - } - expect(error.message).toEqual( - 'Unable to interact with setup ' + - 'mode because the angular injector was not previously set. This needs to be ' + - 'set by calling `initSetupModeState`.' - ); - }); - it('should enable toggle mode', async () => { - await initSetupModeState(angularStateMock.scope, angularStateMock.injector); + const globalState = { + inSetupMode: false, + save: jest.fn(), + }; + const httpServiceMock = { + post: jest.fn(), + }; + + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); toggleSetupMode(true); - expect(injectorModulesMock.globalState.inSetupMode).toBe(true); + expect(globalState.inSetupMode).toBe(true); }); it('should disable toggle mode', async () => { - await initSetupModeState(angularStateMock.scope, angularStateMock.injector); + const globalState = { + inSetupMode: true, + save: jest.fn(), + }; + const httpServiceMock = { + post: jest.fn(), + }; + const handleErrorsMock = jest.fn(); + const callbackMock = jest.fn(); + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); toggleSetupMode(false); - expect(injectorModulesMock.globalState.inSetupMode).toBe(false); + expect(globalState.inSetupMode).toBe(false); }); it('should set top nav config', async () => { + const globalState = { + inSetupMode: false, + save: jest.fn(), + }; + const httpServiceMock = { + post: jest.fn(), + }; + const render = require('react-dom').render; - await initSetupModeState(angularStateMock.scope, angularStateMock.injector); + + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); setSetupModeMenuItem(); toggleSetupMode(true); + expect(render.mock.calls.length).toBe(2); }); }); describe('in setup mode', () => { - afterEach(async () => { - data = {}; - }); - it('should not fetch data if the user does not have sufficient permissions', async () => { + const globalState = { + inSetupMode: false, + save: jest.fn(), + }; + const httpServiceMock = { + post: jest.fn().mockReturnValue( + Promise.resolve({ + _meta: { + hasPermissions: false, + }, + }) + ), + }; + const addDanger = jest.fn(); jest.doMock('../legacy_shims', () => ({ Legacy: { @@ -141,13 +127,9 @@ xdescribe('setup_mode', () => { }, }, })); - data = { - _meta: { - hasPermissions: false, - }, - }; + setModulesAndMocks(); - await initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); toggleSetupMode(true); await waitForSetupModeData(); @@ -160,78 +142,122 @@ xdescribe('setup_mode', () => { }); it('should set the newly discovered cluster uuid', async () => { + const globalState = { + inSetupMode: false, + cluster_uuid: undefined, + save: jest.fn(), + }; const clusterUuid = '1ajy'; - data = { - _meta: { - liveClusterUuid: clusterUuid, - hasPermissions: true, - }, - elasticsearch: { - byUuid: { - 123: { - isPartiallyMigrated: true, + const httpServiceMock = { + post: jest.fn().mockReturnValue( + Promise.resolve({ + _meta: { + liveClusterUuid: clusterUuid, + hasPermissions: true, }, - }, - }, + elasticsearch: { + byUuid: { + 123: { + isPartiallyMigrated: true, + }, + }, + }, + }) + ), }; - await initSetupModeState(angularStateMock.scope, angularStateMock.injector); + + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); toggleSetupMode(true); await waitForSetupModeData(); - expect(injectorModulesMock.globalState.cluster_uuid).toBe(clusterUuid); + expect(globalState.cluster_uuid).toBe(clusterUuid); }); it('should fetch data for a given cluster', async () => { const clusterUuid = '1ajy'; - data = { - _meta: { - liveClusterUuid: clusterUuid, - hasPermissions: true, - }, - elasticsearch: { - byUuid: { - 123: { - isPartiallyMigrated: true, + const globalState = { + inSetupMode: false, + cluster_uuid: clusterUuid, + save: jest.fn(), + }; + const httpServiceMock = { + post: jest.fn().mockReturnValue( + Promise.resolve({ + _meta: { + liveClusterUuid: clusterUuid, + hasPermissions: true, }, - }, - }, + elasticsearch: { + byUuid: { + 123: { + isPartiallyMigrated: true, + }, + }, + }, + }) + ), }; - await initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); toggleSetupMode(true); await waitForSetupModeData(); - expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( + expect(httpServiceMock.post).toHaveBeenCalledWith( `../api/monitoring/v1/setup/collection/cluster/${clusterUuid}`, - { - ccs: undefined, - } + { body: '{}' } ); }); it('should fetch data for a single node', async () => { - await initSetupModeState(angularStateMock.scope, angularStateMock.injector); + const clusterUuid = '1ajy'; + const globalState = { + inSetupMode: false, + save: jest.fn(), + }; + const httpServiceMock = { + post: jest.fn().mockReturnValue( + Promise.resolve({ + _meta: { + liveClusterUuid: clusterUuid, + hasPermissions: true, + }, + elasticsearch: { + byUuid: { + 123: { + isPartiallyMigrated: true, + }, + }, + }, + }) + ), + }; + + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); toggleSetupMode(true); await waitForSetupModeData(); - injectorModulesMock.$http.post.mockClear(); await updateSetupModeData('45asd'); - expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( + expect(httpServiceMock.post).toHaveBeenCalledWith( '../api/monitoring/v1/setup/collection/node/45asd', - { - ccs: undefined, - } + { body: '{}' } ); }); it('should fetch data without a cluster uuid', async () => { - initSetupModeState(angularStateMock.scope, angularStateMock.injector); + const globalState = { + inSetupMode: false, + save: jest.fn(), + }; + const httpServiceMock = { + post: jest.fn(), + }; + + await initSetupModeState(globalState, httpServiceMock, handleErrorsMock, callbackMock); await toggleSetupMode(true); - injectorModulesMock.$http.post.mockClear(); await updateSetupModeData(undefined, true); const url = '../api/monitoring/v1/setup/collection/cluster'; - const args = { ccs: undefined }; - expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args); + const args = { body: '{}' }; + expect(httpServiceMock.post).toHaveBeenCalledWith(url, args); }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index f3f332b5094b6..ceb87429f9de4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; -import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; +import { EuiButtonEmpty, EuiResizableContainer, EuiTitle, EuiPanel } from '@elastic/eui'; import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; @@ -20,6 +20,7 @@ import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; import { SeriesViews } from './views/series_views'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; +import type { ChartTimeRange } from './header/last_updated'; export type PanelId = 'seriesPanel' | 'chartPanel'; @@ -37,7 +38,7 @@ export function ExploratoryView({ const [height, setHeight] = useState('100vh'); - const [lastUpdated, setLastUpdated] = useState(); + const [chartTimeRangeContext, setChartTimeRangeContext] = useState(); const [lensAttributes, setLensAttributes] = useState( null @@ -96,7 +97,10 @@ export function ExploratoryView({ {lens ? ( <> - + {lensAttributes ? ( ) : ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.test.tsx new file mode 100644 index 0000000000000..570362a63c33f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/dom'; +import { render } from '../rtl_helpers'; +import { ChartCreationInfo } from './chart_creation_info'; + +const info = { + to: 1634071132571, + from: 1633406400000, + lastUpdated: 1634071140788, +}; + +describe('ChartCreationInfo', () => { + it('renders chart creation info', async () => { + render(); + + expect(screen.getByText('Chart created')).toBeInTheDocument(); + expect(screen.getByText('Oct 12, 2021 4:39 PM')).toBeInTheDocument(); + expect(screen.getByText('Displaying from')).toBeInTheDocument(); + expect(screen.getByText('Oct 5, 2021 12:00 AM → Oct 12, 2021 4:38 PM')).toBeInTheDocument(); + }); + + it('does not display info when props are falsey', async () => { + render(); + + expect(screen.queryByText('Chart created')).not.toBeInTheDocument(); + expect(screen.queryByText('Displaying from')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.tsx new file mode 100644 index 0000000000000..4814bc8d8630a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_creation_info.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import type { ChartTimeRange } from './last_updated'; + +export function ChartCreationInfo(props: Partial) { + const dateFormat = 'lll'; + const from = moment(props.from).format(dateFormat); + const to = moment(props.to).format(dateFormat); + const created = moment(props.lastUpdated).format(dateFormat); + + return ( + <> + {props.lastUpdated && ( + <> + + + + + + + + {created} + + + + + )} + {props.to && props.from && ( + <> + + + + + + + + + {from} → {to} + + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 181c8342b87af..22245f111293c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -10,16 +10,17 @@ import { i18n } from '@kbn/i18n'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { LastUpdated } from './last_updated'; import { ExpViewActionMenu } from '../components/action_menu'; import { useExpViewTimeRange } from '../hooks/use_time_range'; +import { LastUpdated } from './last_updated'; +import type { ChartTimeRange } from './last_updated'; interface Props { - lastUpdated?: number; + chartTimeRange?: ChartTimeRange; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ lensAttributes, lastUpdated }: Props) { +export function ExploratoryViewHeader({ lensAttributes, chartTimeRange }: Props) { const { setLastRefresh } = useSeriesStorage(); const timeRange = useExpViewTimeRange(); @@ -46,7 +47,7 @@ export function ExploratoryViewHeader({ lensAttributes, lastUpdated }: Props) {
- + setLastRefresh(Date.now())}> diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx index c352ec0423dd8..bc82c48214a01 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx @@ -6,14 +6,24 @@ */ import React, { useEffect, useState } from 'react'; -import { EuiIcon, EuiText } from '@elastic/eui'; import moment from 'moment'; +import styled from 'styled-components'; +import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ChartCreationInfo } from './chart_creation_info'; + +export interface ChartTimeRange { + lastUpdated: number; + to: number; + from: number; +} interface Props { - lastUpdated?: number; + chartTimeRange?: ChartTimeRange; } -export function LastUpdated({ lastUpdated }: Props) { + +export function LastUpdated({ chartTimeRange }: Props) { + const { lastUpdated } = chartTimeRange || {}; const [refresh, setRefresh] = useState(() => Date.now()); useEffect(() => { @@ -39,7 +49,13 @@ export function LastUpdated({ lastUpdated }: Props) { return ( - + } + > + + {' '} ); } + +export const StyledToolTipWrapper = styled.div` + min-width: 30vw; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 235790e72862c..b3ec7ee184f00 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -13,14 +13,16 @@ import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useExpViewTimeRange } from './hooks/use_time_range'; +import { parseRelativeDate } from './components/date_range_picker'; +import type { ChartTimeRange } from './header/last_updated'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; - setLastUpdated: Dispatch>; + setChartTimeRangeContext: Dispatch>; } export function LensEmbeddable(props: Props) { - const { lensAttributes, setLastUpdated } = props; + const { lensAttributes, setChartTimeRangeContext } = props; const { services: { lens, notifications }, @@ -35,8 +37,12 @@ export function LensEmbeddable(props: Props) { const timeRange = useExpViewTimeRange(); const onLensLoad = useCallback(() => { - setLastUpdated(Date.now()); - }, [setLastUpdated]); + setChartTimeRangeContext({ + lastUpdated: Date.now(), + to: parseRelativeDate(timeRange?.to || '').valueOf(), + from: parseRelativeDate(timeRange?.from || '').valueOf(), + }); + }, [setChartTimeRangeContext, timeRange]); const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index d35d18d3b5958..16447e6b0f539 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -41,7 +41,7 @@ import { SPACE_IDS, } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; -import { Dataset, RuleDataPluginService } from '../rule_data_plugin_service'; +import { Dataset, IRuleDataService } from '../rule_data_plugin_service'; const getEsQueryConfig: typeof getEsQueryConfigTyped = getEsQueryConfigNonTyped; const getSafeSortIds: typeof getSafeSortIdsTyped = getSafeSortIdsNonTyped; @@ -71,7 +71,7 @@ export interface ConstructorOptions { authorization: PublicMethodsOf; auditLogger?: AuditLogger; esClient: ElasticsearchClient; - ruleDataService: RuleDataPluginService; + ruleDataService: IRuleDataService; } export interface UpdateOptions { @@ -116,7 +116,7 @@ export class AlertsClient { private readonly authorization: PublicMethodsOf; private readonly esClient: ElasticsearchClient; private readonly spaceId: string | undefined; - private readonly ruleDataService: RuleDataPluginService; + private readonly ruleDataService: IRuleDataService; constructor(options: ConstructorOptions) { this.logger = options.logger; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts index 41ef5e4edb0d1..276ea070d6f87 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts @@ -13,8 +13,7 @@ import { loggingSystemMock } from 'src/core/server/mocks'; import { securityMock } from '../../../security/server/mocks'; import { AuditLogger } from '../../../security/server'; import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; -import { ruleDataPluginServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock'; -import { RuleDataPluginService } from '../rule_data_plugin_service'; +import { ruleDataServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock'; jest.mock('./alerts_client'); @@ -26,7 +25,7 @@ const alertsClientFactoryParams: AlertsClientFactoryProps = { getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock, securityPluginSetup, esClient: {} as ElasticsearchClient, - ruleDataService: ruleDataPluginServiceMock.create() as unknown as RuleDataPluginService, + ruleDataService: ruleDataServiceMock.create(), }; const fakeRequest = { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts index c1ff6d5d56ea9..8225394c2dba7 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.ts @@ -9,7 +9,7 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, KibanaRequest, Logger } from 'src/core/server'; import { AlertingAuthorization } from '../../../alerting/server'; import { SecurityPluginSetup } from '../../../security/server'; -import { RuleDataPluginService } from '../rule_data_plugin_service'; +import { IRuleDataService } from '../rule_data_plugin_service'; import { AlertsClient } from './alerts_client'; export interface AlertsClientFactoryProps { @@ -17,7 +17,7 @@ export interface AlertsClientFactoryProps { esClient: ElasticsearchClient; getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf; securityPluginSetup: SecurityPluginSetup | undefined; - ruleDataService: RuleDataPluginService | null; + ruleDataService: IRuleDataService | null; } export class AlertsClientFactory { @@ -28,7 +28,7 @@ export class AlertsClientFactory { request: KibanaRequest ) => PublicMethodsOf; private securityPluginSetup!: SecurityPluginSetup | undefined; - private ruleDataService!: RuleDataPluginService | null; + private ruleDataService!: IRuleDataService | null; public initialize(options: AlertsClientFactoryProps) { /** diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts index 2be1f6875cd7e..8868d7959621d 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts @@ -19,8 +19,7 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; import { AuditLogger } from '../../../../security/server'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; -import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; -import { RuleDataPluginService } from '../../rule_data_plugin_service'; +import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); @@ -33,7 +32,7 @@ const alertsClientParams: jest.Mocked = { authorization: alertingAuthMock, esClient: esClientMock, auditLogger, - ruleDataService: ruleDataPluginServiceMock.create() as unknown as RuleDataPluginService, + ruleDataService: ruleDataServiceMock.create(), }; const DEFAULT_SPACE = 'test_default_space_id'; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts index b94a3b96312e4..5f9a20c14ea5b 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts @@ -18,8 +18,7 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; import { AuditLogger } from '../../../../security/server'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; -import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; -import { RuleDataPluginService } from '../../rule_data_plugin_service'; +import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); @@ -32,7 +31,7 @@ const alertsClientParams: jest.Mocked = { authorization: alertingAuthMock, esClient: esClientMock, auditLogger, - ruleDataService: ruleDataPluginServiceMock.create() as unknown as RuleDataPluginService, + ruleDataService: ruleDataServiceMock.create(), }; const DEFAULT_SPACE = 'test_default_space_id'; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index 320e9f8a5fb1c..eaf6c0089ce12 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -19,8 +19,7 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; import { AuditLogger } from '../../../../security/server'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; -import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; -import { RuleDataPluginService } from '../../rule_data_plugin_service'; +import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); @@ -33,7 +32,7 @@ const alertsClientParams: jest.Mocked = { authorization: alertingAuthMock, esClient: esClientMock, auditLogger, - ruleDataService: ruleDataPluginServiceMock.create() as unknown as RuleDataPluginService, + ruleDataService: ruleDataServiceMock.create(), }; const DEFAULT_SPACE = 'test_default_space_id'; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index 922011dcb5271..85527e26a9cd3 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -18,8 +18,7 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; import { AuditLogger } from '../../../../security/server'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; -import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; -import { RuleDataPluginService } from '../../rule_data_plugin_service'; +import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); @@ -32,7 +31,7 @@ const alertsClientParams: jest.Mocked = { authorization: alertingAuthMock, esClient: esClientMock, auditLogger, - ruleDataService: ruleDataPluginServiceMock.create() as unknown as RuleDataPluginService, + ruleDataService: ruleDataServiceMock.create(), }; const DEFAULT_SPACE = 'test_default_space_id'; diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 5331ab86be982..d6c5b61706415 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -12,7 +12,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { RuleRegistryPlugin } from './plugin'; export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin'; -export { RuleDataPluginService } from './rule_data_plugin_service'; +export { IRuleDataService, RuleDataPluginService } from './rule_data_plugin_service'; export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export type { diff --git a/x-pack/plugins/rule_registry/server/mocks.ts b/x-pack/plugins/rule_registry/server/mocks.ts index e9ec25ddcdaba..023de6aa6029c 100644 --- a/x-pack/plugins/rule_registry/server/mocks.ts +++ b/x-pack/plugins/rule_registry/server/mocks.ts @@ -7,12 +7,17 @@ import { alertsClientMock } from './alert_data_client/alerts_client.mock'; import { createRuleDataClientMock } from './rule_data_client/rule_data_client.mock'; -import { ruleDataPluginServiceMock } from './rule_data_plugin_service/rule_data_plugin_service.mock'; +import { + ruleDataServiceMock, + RuleDataServiceMock, +} from './rule_data_plugin_service/rule_data_plugin_service.mock'; import { createLifecycleAlertServicesMock } from './utils/lifecycle_alert_services_mock'; export const ruleRegistryMocks = { createLifecycleAlertServices: createLifecycleAlertServicesMock, - createRuleDataPluginService: ruleDataPluginServiceMock.create, + createRuleDataService: ruleDataServiceMock.create, createRuleDataClient: createRuleDataClientMock, createAlertsClientMock: alertsClientMock, }; + +export { RuleDataServiceMock }; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index b68f3eeb10669..334216ce41361 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -20,7 +20,7 @@ import { PluginStartContract as AlertingStart } from '../../alerting/server'; import { SecurityPluginSetup } from '../../security/server'; import { RuleRegistryPluginConfig } from './config'; -import { RuleDataPluginService } from './rule_data_plugin_service'; +import { IRuleDataService, RuleDataService } from './rule_data_plugin_service'; import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; import { AlertsClient } from './alert_data_client/alerts_client'; import { RacApiRequestHandlerContext, RacRequestHandlerContext } from './types'; @@ -35,7 +35,7 @@ export interface RuleRegistryPluginStartDependencies { } export interface RuleRegistryPluginSetupContract { - ruleDataService: RuleDataPluginService; + ruleDataService: IRuleDataService; } export interface RuleRegistryPluginStartContract { @@ -57,7 +57,7 @@ export class RuleRegistryPlugin private readonly logger: Logger; private readonly kibanaVersion: string; private readonly alertsClientFactory: AlertsClientFactory; - private ruleDataService: RuleDataPluginService | null; + private ruleDataService: IRuleDataService | null; private security: SecurityPluginSetup | undefined; constructor(initContext: PluginInitializerContext) { @@ -100,7 +100,7 @@ export class RuleRegistryPlugin } }; - this.ruleDataService = new RuleDataPluginService({ + this.ruleDataService = new RuleDataService({ logger, kibanaVersion, isWriteEnabled: isWriteEnabled(this.config, this.legacyConfig), diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts index c50a982741b0c..43e727e79b76b 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts @@ -5,13 +5,10 @@ * 2.0. */ -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { RuleDataPluginService } from './rule_data_plugin_service'; +import { IRuleDataService } from './rule_data_plugin_service'; -type Schema = PublicMethodsOf; - -const createRuleDataPluginService = () => { - const mocked: jest.Mocked = { +export const ruleDataServiceMock = { + create: (): jest.Mocked => ({ getResourcePrefix: jest.fn(), getResourceName: jest.fn(), isWriteEnabled: jest.fn(), @@ -19,10 +16,9 @@ const createRuleDataPluginService = () => { initializeIndex: jest.fn(), findIndexByName: jest.fn(), findIndicesByFeature: jest.fn(), - }; - return mocked; + }), }; -export const ruleDataPluginServiceMock = { - create: createRuleDataPluginService, -}; +export const RuleDataServiceMock = jest + .fn, []>() + .mockImplementation(ruleDataServiceMock.create); diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts index 0617bc0a820ac..c5ec38ec8534e 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts @@ -17,6 +17,59 @@ import { Dataset, IndexOptions } from './index_options'; import { ResourceInstaller } from './resource_installer'; import { joinWithDash } from './utils'; +/** + * A service for creating and using Elasticsearch indices for alerts-as-data. + */ +export interface IRuleDataService { + /** + * Returns a prefix used in the naming scheme of index aliases, templates + * and other Elasticsearch resources that this service creates + * for alerts-as-data indices. + */ + getResourcePrefix(): string; + + /** + * Prepends a relative resource name with the resource prefix. + * @returns Full name of the resource. + * @example 'security.alerts' => '.alerts-security.alerts' + */ + getResourceName(relativeName: string): string; + + /** + * If write is enabled, everything works as usual. + * If it's disabled, writing to all alerts-as-data indices will be disabled, + * and also Elasticsearch resources associated with the indices will not be + * installed. + */ + isWriteEnabled(): boolean; + + /** + * Installs common Elasticsearch resources used by all alerts-as-data indices. + */ + initializeService(): void; + + /** + * Initializes alerts-as-data index and starts index bootstrapping right away. + * @param indexOptions Index parameters: names and resources. + * @returns Client for reading and writing data to this index. + */ + initializeIndex(indexOptions: IndexOptions): IRuleDataClient; + + /** + * Looks up the index information associated with the given registration context and dataset. + */ + findIndexByName(registrationContext: string, dataset: Dataset): IndexInfo | null; + + /** + * Looks up the index information associated with the given Kibana "feature". + * Note: features are used in RBAC. + */ + findIndicesByFeature(featureId: ValidFeatureId, dataset?: Dataset): IndexInfo[]; +} + +// TODO: This is a leftover. Remove its usage from the "observability" plugin and delete it. +export type RuleDataPluginService = IRuleDataService; + interface ConstructorOptions { getClusterClient: () => Promise; logger: Logger; @@ -24,10 +77,7 @@ interface ConstructorOptions { isWriteEnabled: boolean; } -/** - * A service for creating and using Elasticsearch indices for alerts-as-data. - */ -export class RuleDataPluginService { +export class RuleDataService implements IRuleDataService { private readonly indicesByBaseName: Map; private readonly indicesByFeatureId: Map; private readonly resourceInstaller: ResourceInstaller; @@ -49,37 +99,18 @@ export class RuleDataPluginService { this.isInitialized = false; } - /** - * Returns a prefix used in the naming scheme of index aliases, templates - * and other Elasticsearch resources that this service creates - * for alerts-as-data indices. - */ public getResourcePrefix(): string { return INDEX_PREFIX; } - /** - * Prepends a relative resource name with the resource prefix. - * @returns Full name of the resource. - * @example 'security.alerts' => '.alerts-security.alerts' - */ public getResourceName(relativeName: string): string { return joinWithDash(this.getResourcePrefix(), relativeName); } - /** - * If write is enabled, everything works as usual. - * If it's disabled, writing to all alerts-as-data indices will be disabled, - * and also Elasticsearch resources associated with the indices will not be - * installed. - */ public isWriteEnabled(): boolean { return this.options.isWriteEnabled; } - /** - * Installs common Elasticsearch resources used by all alerts-as-data indices. - */ public initializeService(): void { // Run the installation of common resources and handle exceptions. this.installCommonResources = this.resourceInstaller @@ -93,11 +124,6 @@ export class RuleDataPluginService { this.isInitialized = true; } - /** - * Initializes alerts-as-data index and starts index bootstrapping right away. - * @param indexOptions Index parameters: names and resources. - * @returns Client for reading and writing data to this index. - */ public initializeIndex(indexOptions: IndexOptions): IRuleDataClient { if (!this.isInitialized) { throw new Error( @@ -156,18 +182,11 @@ export class RuleDataPluginService { }); } - /** - * Looks up the index information associated with the given registration context and dataset. - */ public findIndexByName(registrationContext: string, dataset: Dataset): IndexInfo | null { const baseName = this.getResourceName(`${registrationContext}.${dataset}`); return this.indicesByBaseName.get(baseName) ?? null; } - /** - * Looks up the index information associated with the given Kibana "feature". - * Note: features are used in RBAC. - */ public findIndicesByFeature(featureId: ValidFeatureId, dataset?: Dataset): IndexInfo[] { const foundIndices = this.indicesByFeatureId.get(featureId) ?? []; return dataset ? foundIndices.filter((i) => i.indexOptions.dataset === dataset) : foundIndices; diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts index ac5d252c98a8b..1d05036191635 100644 --- a/x-pack/plugins/security/common/index.ts +++ b/x-pack/plugins/security/common/index.ts @@ -6,4 +6,4 @@ */ export type { SecurityLicense } from './licensing'; -export type { AuthenticatedUser } from './model'; +export type { AuthenticatedUser, PrivilegeDeprecationsService } from './model'; diff --git a/x-pack/plugins/security/server/deprecations/index.ts b/x-pack/plugins/security/server/deprecations/index.ts index 05802a5a673c5..2c4b47ba41a0a 100644 --- a/x-pack/plugins/security/server/deprecations/index.ts +++ b/x-pack/plugins/security/server/deprecations/index.ts @@ -5,8 +5,9 @@ * 2.0. */ -/** - * getKibanaRolesByFeature - */ - export { getPrivilegeDeprecationsService } from './privilege_deprecations'; +export { + registerKibanaUserRoleDeprecation, + KIBANA_ADMIN_ROLE_NAME, + KIBANA_USER_ROLE_NAME, +} from './kibana_user_role'; diff --git a/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts new file mode 100644 index 0000000000000..da728b12fca91 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts @@ -0,0 +1,328 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import type { SecurityRoleMapping, SecurityUser } from '@elastic/elasticsearch/api/types'; + +import type { PackageInfo, RegisterDeprecationsConfig } from 'src/core/server'; +import { + deprecationsServiceMock, + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; + +import { licenseMock } from '../../common/licensing/index.mock'; +import { securityMock } from '../mocks'; +import { registerKibanaUserRoleDeprecation } from './kibana_user_role'; + +function getDepsMock() { + return { + logger: loggingSystemMock.createLogger(), + deprecationsService: deprecationsServiceMock.createSetupContract(), + license: licenseMock.create(), + packageInfo: { + branch: 'some-branch', + buildSha: 'sha', + dist: true, + version: '8.0.0', + buildNum: 1, + } as PackageInfo, + }; +} + +function getContextMock() { + return { + esClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + }; +} + +function createMockUser(user: Partial = {}) { + return { enabled: true, username: 'userA', roles: ['roleA'], metadata: {}, ...user }; +} + +function createMockRoleMapping(mapping: Partial = {}) { + return { enabled: true, roles: ['roleA'], rules: {}, metadata: {}, ...mapping }; +} + +describe('Kibana Dashboard Only User role deprecations', () => { + let mockDeps: ReturnType; + let mockContext: ReturnType; + let deprecationHandler: RegisterDeprecationsConfig; + beforeEach(() => { + mockContext = getContextMock(); + mockDeps = getDepsMock(); + registerKibanaUserRoleDeprecation(mockDeps); + + expect(mockDeps.deprecationsService.registerDeprecations).toHaveBeenCalledTimes(1); + deprecationHandler = mockDeps.deprecationsService.registerDeprecations.mock.calls[0][0]; + }); + + it('does not return any deprecations if security is not enabled', async () => { + mockDeps.license.isEnabled.mockReturnValue(false); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]); + expect(mockContext.esClient.asCurrentUser.security.getUser).not.toHaveBeenCalled(); + expect(mockContext.esClient.asCurrentUser.security.getRoleMapping).not.toHaveBeenCalled(); + }); + + it('does not return any deprecations if none of the users and role mappings has a kibana user role', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: { enabled: true, roles: ['roleA'], rules: {}, metadata: {} }, + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]); + }); + + it('returns deprecations even if cannot retrieve users due to permission error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} })) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure you have a \\"manage_security\\" cluster privilege assigned.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7", + "level": "fetch_error", + "message": "You do not have enough permissions to fix this deprecation.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve users due to unknown error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} })) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Check Kibana logs for more details.", + ], + }, + "deprecationType": "feature", + "level": "fetch_error", + "message": "Failed to perform deprecation check. Check Kibana logs for more details.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve role mappings due to permission error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} })) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure you have a \\"manage_security\\" cluster privilege assigned.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7", + "level": "fetch_error", + "message": "You do not have enough permissions to fix this deprecation.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve role mappings due to unknown error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} })) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Check Kibana logs for more details.", + ], + }, + "deprecationType": "feature", + "level": "fetch_error", + "message": "Failed to perform deprecation check. Check Kibana logs for more details.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns only user-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }), + }, + }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_users", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all users and add the \\"kibana_admin\\" role. The affected users are: userB, userD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns only role-mapping-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_user'] }), + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_role_mappings", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all role mappings and add the \\"kibana_admin\\" role. The affected role mappings are: mappingB, mappingD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns both user-related and role-mapping-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }), + }, + }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_user'] }), + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_users", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all users and add the \\"kibana_admin\\" role. The affected users are: userB, userD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_role_mappings", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all role mappings and add the \\"kibana_admin\\" role. The affected role mappings are: mappingB, mappingD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/security/server/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/deprecations/kibana_user_role.ts new file mode 100644 index 0000000000000..d659ea273f05f --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/kibana_user_role.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SecurityGetRoleMappingResponse, + SecurityGetUserResponse, +} from '@elastic/elasticsearch/api/types'; + +import { i18n } from '@kbn/i18n'; +import type { + DeprecationsDetails, + DeprecationsServiceSetup, + ElasticsearchClient, + Logger, + PackageInfo, +} from 'src/core/server'; + +import type { SecurityLicense } from '../../common'; +import { getDetailedErrorMessage, getErrorStatusCode } from '../errors'; + +export const KIBANA_USER_ROLE_NAME = 'kibana_user'; +export const KIBANA_ADMIN_ROLE_NAME = 'kibana_admin'; + +interface Deps { + deprecationsService: DeprecationsServiceSetup; + license: SecurityLicense; + logger: Logger; + packageInfo: PackageInfo; +} + +function getDeprecationTitle() { + return i18n.translate('xpack.security.deprecations.kibanaUser.deprecationTitle', { + defaultMessage: 'The "{userRoleName}" role is deprecated', + values: { userRoleName: KIBANA_USER_ROLE_NAME }, + }); +} + +function getDeprecationMessage() { + return i18n.translate('xpack.security.deprecations.kibanaUser.deprecationMessage', { + defaultMessage: + 'Use the "{adminRoleName}" role to grant access to all Kibana features in all spaces.', + values: { adminRoleName: KIBANA_ADMIN_ROLE_NAME }, + }); +} + +export const registerKibanaUserRoleDeprecation = ({ + deprecationsService, + logger, + license, + packageInfo, +}: Deps) => { + deprecationsService.registerDeprecations({ + getDeprecations: async (context) => { + // Nothing to do if security is disabled + if (!license.isEnabled()) { + return []; + } + + return [ + ...(await getUsersDeprecations(context.esClient.asCurrentUser, logger, packageInfo)), + ...(await getRoleMappingsDeprecations(context.esClient.asCurrentUser, logger, packageInfo)), + ]; + }, + }); +}; + +async function getUsersDeprecations( + client: ElasticsearchClient, + logger: Logger, + packageInfo: PackageInfo +): Promise { + let users: SecurityGetUserResponse; + try { + users = (await client.security.getUser()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve users when checking for deprecations: the "manage_security" cluster privilege is required.` + ); + } else { + logger.error( + `Failed to retrieve users when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}.` + ); + } + return deprecationError(packageInfo, err); + } + + const usersWithKibanaUserRole = Object.values(users) + .filter((user) => user.roles.includes(KIBANA_USER_ROLE_NAME)) + .map((user) => user.username); + if (usersWithKibanaUserRole.length === 0) { + return []; + } + + return [ + { + title: getDeprecationTitle(), + message: getDeprecationMessage(), + level: 'warning', + deprecationType: 'feature', + documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`, + correctiveActions: { + api: { + method: 'POST', + path: '/internal/security/deprecations/kibana_user_role/_fix_users', + }, + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaUser.usersDeprecationCorrectiveAction', + { + defaultMessage: + 'Remove the "{userRoleName}" role from all users and add the "{adminRoleName}" role. The affected users are: {users}.', + values: { + userRoleName: KIBANA_USER_ROLE_NAME, + adminRoleName: KIBANA_ADMIN_ROLE_NAME, + users: usersWithKibanaUserRole.join(', '), + }, + } + ), + ], + }, + }, + ]; +} + +async function getRoleMappingsDeprecations( + client: ElasticsearchClient, + logger: Logger, + packageInfo: PackageInfo +): Promise { + let roleMappings: SecurityGetRoleMappingResponse; + try { + roleMappings = (await client.security.getRoleMapping()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve role mappings when checking for deprecations: the "manage_security" cluster privilege is required.` + ); + } else { + logger.error( + `Failed to retrieve role mappings when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}.` + ); + } + return deprecationError(packageInfo, err); + } + + const roleMappingsWithKibanaUserRole = Object.entries(roleMappings) + .filter(([, roleMapping]) => roleMapping.roles.includes(KIBANA_USER_ROLE_NAME)) + .map(([mappingName]) => mappingName); + if (roleMappingsWithKibanaUserRole.length === 0) { + return []; + } + + return [ + { + title: getDeprecationTitle(), + message: getDeprecationMessage(), + level: 'warning', + deprecationType: 'feature', + documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`, + correctiveActions: { + api: { + method: 'POST', + path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings', + }, + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaUser.roleMappingsDeprecationCorrectiveAction', + { + defaultMessage: + 'Remove the "{userRoleName}" role from all role mappings and add the "{adminRoleName}" role. The affected role mappings are: {roleMappings}.', + values: { + userRoleName: KIBANA_USER_ROLE_NAME, + adminRoleName: KIBANA_ADMIN_ROLE_NAME, + roleMappings: roleMappingsWithKibanaUserRole.join(', '), + }, + } + ), + ], + }, + }, + ]; +} + +function deprecationError(packageInfo: PackageInfo, error: Error): DeprecationsDetails[] { + const title = getDeprecationTitle(); + + if (getErrorStatusCode(error) === 403) { + return [ + { + title, + level: 'fetch_error', + deprecationType: 'feature', + message: i18n.translate('xpack.security.deprecations.kibanaUser.forbiddenErrorMessage', { + defaultMessage: 'You do not have enough permissions to fix this deprecation.', + }), + documentationUrl: `https://www.elastic.co/guide/en/kibana/${packageInfo.branch}/xpack-security.html#_required_permissions_7`, + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaUser.forbiddenErrorCorrectiveAction', + { + defaultMessage: + 'Make sure you have a "manage_security" cluster privilege assigned.', + } + ), + ], + }, + }, + ]; + } + + return [ + { + title, + level: 'fetch_error', + deprecationType: 'feature', + message: i18n.translate('xpack.security.deprecations.kibanaUser.unknownErrorMessage', { + defaultMessage: 'Failed to perform deprecation check. Check Kibana logs for more details.', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.security.deprecations.kibanaUser.unknownErrorCorrectiveAction', { + defaultMessage: 'Check Kibana logs for more details.', + }), + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 98e77038f168a..1e42d10b205aa 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -27,9 +27,8 @@ import type { import type { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import type { SecurityLicense } from '../common/licensing'; +import type { AuthenticatedUser, PrivilegeDeprecationsService, SecurityLicense } from '../common'; import { SecurityLicenseService } from '../common/licensing'; -import type { AuthenticatedUser, PrivilegeDeprecationsService } from '../common/model'; import type { AnonymousAccessServiceStart } from './anonymous_access'; import { AnonymousAccessService } from './anonymous_access'; import type { AuditServiceSetup } from './audit'; @@ -43,7 +42,7 @@ import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal } fro import { AuthorizationService } from './authorization'; import type { ConfigSchema, ConfigType } from './config'; import { createConfig } from './config'; -import { getPrivilegeDeprecationsService } from './deprecations'; +import { getPrivilegeDeprecationsService, registerKibanaUserRoleDeprecation } from './deprecations'; import { ElasticsearchService } from './elasticsearch'; import type { SecurityFeatureUsageServiceStart } from './feature_usage'; import { SecurityFeatureUsageService } from './feature_usage'; @@ -290,6 +289,8 @@ export class SecurityPlugin getSpacesService: () => spaces?.spacesService, }); + this.registerDeprecations(core, license); + defineRoutes({ router: core.http.createRouter(), basePath: core.http.basePath, @@ -414,4 +415,14 @@ export class SecurityPlugin this.authorizationService.stop(); this.sessionManagementService.stop(); } + + private registerDeprecations(core: CoreSetup, license: SecurityLicense) { + const logger = this.logger.get('deprecations'); + registerKibanaUserRoleDeprecation({ + deprecationsService: core.deprecations, + license, + logger, + packageInfo: this.initializerContext.env.packageInfo, + }); + } } diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/index.ts b/x-pack/plugins/security/server/routes/deprecations/index.ts similarity index 50% rename from x-pack/plugins/monitoring/public/application/setup_mode/index.ts rename to x-pack/plugins/security/server/routes/deprecations/index.ts index 57d734fc6d056..cbc186ed2e925 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/index.ts +++ b/x-pack/plugins/security/server/routes/deprecations/index.ts @@ -5,4 +5,9 @@ * 2.0. */ -export * from '../../lib/setup_mode'; +import type { RouteDefinitionParams } from '../'; +import { defineKibanaUserRoleDeprecationRoutes } from './kibana_user_role'; + +export function defineDeprecationsRoutes(params: RouteDefinitionParams) { + defineKibanaUserRoleDeprecationRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts new file mode 100644 index 0000000000000..b2ae2543bd652 --- /dev/null +++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import type { SecurityRoleMapping, SecurityUser } from '@elastic/elasticsearch/api/types'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { RequestHandler, RouteConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; + +import { securityMock } from '../../mocks'; +import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { defineKibanaUserRoleDeprecationRoutes } from './kibana_user_role'; + +function createMockUser(user: Partial = {}) { + return { enabled: true, username: 'userA', roles: ['roleA'], metadata: {}, ...user }; +} + +function createMockRoleMapping(mapping: Partial = {}) { + return { enabled: true, roles: ['roleA'], rules: {}, metadata: {}, ...mapping }; +} + +describe('Kibana user deprecation routes', () => { + let router: jest.Mocked; + let mockContext: DeeplyMockedKeys; + beforeEach(() => { + const routeParamsMock = routeDefinitionParamsMock.create(); + router = routeParamsMock.router; + + mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } }, + } as any; + + defineKibanaUserRoleDeprecationRoutes(routeParamsMock); + }); + + describe('Users with Kibana user role', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + beforeEach(() => { + const [fixUsersRouteConfig, fixUsersRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/security/deprecations/kibana_user_role/_fix_users' + )!; + + routeConfig = fixUsersRouteConfig; + routeHandler = fixUsersRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('fails if cannot retrieve users', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).not.toHaveBeenCalled(); + }); + + it('fails if fails to update user', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA', 'kibana_user'] }), + userB: createMockUser({ username: 'userB', roles: ['kibana_user'] }), + }, + }) + ); + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledTimes(1); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userA', + body: createMockUser({ username: 'userA', roles: ['roleA', 'kibana_admin'] }), + }); + }); + + it('does nothing if there are no users with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).not.toHaveBeenCalled(); + }); + + it('updates users with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }), + userE: createMockUser({ + username: 'userE', + roles: ['kibana_user', 'kibana_admin', 'roleE'], + }), + }, + }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledTimes(3); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userB', + body: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userD', + body: createMockUser({ username: 'userD', roles: ['kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userE', + body: createMockUser({ username: 'userE', roles: ['kibana_admin', 'roleE'] }), + }); + }); + }); + + describe('Role mappings with Kibana user role', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + beforeEach(() => { + const [fixRoleMappingsRouteConfig, fixRoleMappingsRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => + path === '/internal/security/deprecations/kibana_user_role/_fix_role_mappings' + )!; + + routeConfig = fixRoleMappingsRouteConfig; + routeHandler = fixRoleMappingsRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('fails if cannot retrieve role mappings', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).not.toHaveBeenCalled(); + }); + + it('fails if fails to update role mapping', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA', 'kibana_user'] }), + mappingB: createMockRoleMapping({ roles: ['kibana_user'] }), + }, + }) + ); + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledTimes(1); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingA', + body: createMockRoleMapping({ roles: ['roleA', 'kibana_admin'] }), + }); + }); + + it('does nothing if there are no role mappings with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).not.toHaveBeenCalled(); + }); + + it('updates role mappings with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_user'] }), + mappingE: createMockRoleMapping({ roles: ['kibana_user', 'kibana_admin', 'roleE'] }), + }, + }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledTimes(3); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingB', + body: createMockRoleMapping({ roles: ['roleB', 'kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingD', + body: createMockRoleMapping({ roles: ['kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingE', + body: createMockRoleMapping({ roles: ['kibana_admin', 'roleE'] }), + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts new file mode 100644 index 0000000000000..21bb9db7329b6 --- /dev/null +++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SecurityGetRoleMappingResponse, + SecurityGetUserResponse, +} from '@elastic/elasticsearch/api/types'; + +import type { RouteDefinitionParams } from '..'; +import { KIBANA_ADMIN_ROLE_NAME, KIBANA_USER_ROLE_NAME } from '../../deprecations'; +import { + getDetailedErrorMessage, + getErrorStatusCode, + wrapIntoCustomErrorResponse, +} from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +/** + * Defines routes required to handle `kibana_user` deprecation. + */ +export function defineKibanaUserRoleDeprecationRoutes({ router, logger }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/deprecations/kibana_user_role/_fix_users', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + let users: SecurityGetUserResponse; + try { + users = (await context.core.elasticsearch.client.asCurrentUser.security.getUser()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve users when checking for deprecations: the manage_security cluster privilege is required` + ); + } else { + logger.error( + `Failed to retrieve users when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}` + ); + } + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + const usersWithKibanaUserRole = Object.values(users).filter((user) => + user.roles.includes(KIBANA_USER_ROLE_NAME) + ); + + if (usersWithKibanaUserRole.length === 0) { + logger.debug(`No users with "${KIBANA_USER_ROLE_NAME}" role found.`); + } else { + logger.debug( + `The following users with "${KIBANA_USER_ROLE_NAME}" role found and will be migrated to "${KIBANA_ADMIN_ROLE_NAME}" role: ${usersWithKibanaUserRole + .map((user) => user.username) + .join(', ')}.` + ); + } + + for (const userToUpdate of usersWithKibanaUserRole) { + const roles = userToUpdate.roles.filter((role) => role !== KIBANA_USER_ROLE_NAME); + if (!roles.includes(KIBANA_ADMIN_ROLE_NAME)) { + roles.push(KIBANA_ADMIN_ROLE_NAME); + } + + try { + await context.core.elasticsearch.client.asCurrentUser.security.putUser({ + username: userToUpdate.username, + body: { ...userToUpdate, roles }, + }); + } catch (err) { + logger.error( + `Failed to update user "${userToUpdate.username}": ${getDetailedErrorMessage(err)}.` + ); + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + logger.debug(`Successfully updated user "${userToUpdate.username}".`); + } + + return response.ok({ body: {} }); + }) + ); + + router.post( + { + path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + let roleMappings: SecurityGetRoleMappingResponse; + try { + roleMappings = ( + await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping() + ).body; + } catch (err) { + logger.error(`Failed to retrieve role mappings: ${getDetailedErrorMessage(err)}.`); + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + const roleMappingsWithKibanaUserRole = Object.entries(roleMappings).filter(([, mapping]) => + mapping.roles.includes(KIBANA_USER_ROLE_NAME) + ); + + if (roleMappingsWithKibanaUserRole.length === 0) { + logger.debug(`No role mappings with "${KIBANA_USER_ROLE_NAME}" role found.`); + } else { + logger.debug( + `The following role mappings with "${KIBANA_USER_ROLE_NAME}" role found and will be migrated to "${KIBANA_ADMIN_ROLE_NAME}" role: ${roleMappingsWithKibanaUserRole + .map(([mappingName]) => mappingName) + .join(', ')}.` + ); + } + + for (const [mappingNameToUpdate, mappingToUpdate] of roleMappingsWithKibanaUserRole) { + const roles = mappingToUpdate.roles.filter((role) => role !== KIBANA_USER_ROLE_NAME); + if (!roles.includes(KIBANA_ADMIN_ROLE_NAME)) { + roles.push(KIBANA_ADMIN_ROLE_NAME); + } + + try { + await context.core.elasticsearch.client.asCurrentUser.security.putRoleMapping({ + name: mappingNameToUpdate, + body: { ...mappingToUpdate, roles }, + }); + } catch (err) { + logger.error( + `Failed to update role mapping "${mappingNameToUpdate}": ${getDetailedErrorMessage( + err + )}.` + ); + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + logger.debug(`Successfully updated role mapping "${mappingNameToUpdate}".`); + } + + return response.ok({ body: {} }); + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 851e70a357cf9..6785fe57c6b32 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -11,7 +11,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { HttpResources, IBasePath, Logger } from 'src/core/server'; import type { KibanaFeature } from '../../../features/server'; -import type { SecurityLicense } from '../../common/licensing'; +import type { SecurityLicense } from '../../common'; import type { AnonymousAccessServiceStart } from '../anonymous_access'; import type { InternalAuthenticationServiceStart } from '../authentication'; import type { AuthorizationServiceSetupInternal } from '../authorization'; @@ -23,6 +23,7 @@ import { defineAnonymousAccessRoutes } from './anonymous_access'; import { defineApiKeysRoutes } from './api_keys'; import { defineAuthenticationRoutes } from './authentication'; import { defineAuthorizationRoutes } from './authorization'; +import { defineDeprecationsRoutes } from './deprecations'; import { defineIndicesRoutes } from './indices'; import { defineRoleMappingRoutes } from './role_mapping'; import { defineSecurityCheckupGetStateRoutes } from './security_checkup'; @@ -58,6 +59,7 @@ export function defineRoutes(params: RouteDefinitionParams) { defineUsersRoutes(params); defineRoleMappingRoutes(params); defineViewRoutes(params); + defineDeprecationsRoutes(params); defineAnonymousAccessRoutes(params); defineSecurityCheckupGetStateRoutes(params); } diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_actions.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_actions.ts index 246cf059f4cbd..5cc564ee3d41d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_actions.ts @@ -66,7 +66,11 @@ export const indexFleetActionsForHost = async ( const actionResponse = fleetActionGenerator.generateResponse({ action_id: action.action_id, agent_id: agentId, - action_data: action.data, + action_data: { + ...action.data, + // add ack to 4/5th of fleet response + ack: fleetActionGenerator.randomFloat() < 0.8 ? true : undefined, + }, }); esClient diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index fb29297eb5929..d7ad417fc7d3f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -64,6 +64,7 @@ export interface LogsEndpointActionResponse { export interface EndpointActionData { command: ISOLATION_ACTIONS; comment?: string; + ack?: boolean; } export interface EndpointAction { diff --git a/x-pack/plugins/security_solution/cypress/downloads/test_exception_list.ndjson b/x-pack/plugins/security_solution/cypress/downloads/test_exception_list.ndjson deleted file mode 100644 index 54420eff29e0d..0000000000000 --- a/x-pack/plugins/security_solution/cypress/downloads/test_exception_list.ndjson +++ /dev/null @@ -1,2 +0,0 @@ -{"_version":"WzQyNjA0LDFd","created_at":"2021-10-14T01:30:22.034Z","created_by":"elastic","description":"Test exception list description","id":"4c65a230-2c8e-11ec-be1c-2bbdec602f88","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"b04983b4-1617-441c-bb6c-c729281fa2e9","type":"detection","updated_at":"2021-10-14T01:30:22.036Z","updated_by":"elastic","version":1} -{"exported_list_items_count":0} diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index 69b623de0b43c..287d86c6fba9e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -20,8 +20,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { CASES_URL } from '../../urls/navigation'; -// Skipping flakey test: https://github.com/elastic/kibana/issues/115438 -describe.skip('Cases connectors', () => { +describe('Cases connectors', () => { const configureResult = { connector: { id: 'e271c3b8-f702-4fbc-98e0-db942b573bbd', diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts index b772924697148..1a70bb1038320 100644 --- a/x-pack/plugins/security_solution/cypress/objects/exception.ts +++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts @@ -41,5 +41,5 @@ export const expectedExportedExceptionList = ( exceptionListResponse: Cypress.Response ): string => { const jsonrule = exceptionListResponse.body; - return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"detection","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n{"exported_list_items_count":0}\n`; + return `{"_version":"${jsonrule._version}","created_at":"${jsonrule.created_at}","created_by":"elastic","description":"${jsonrule.description}","id":"${jsonrule.id}","immutable":false,"list_id":"test_exception_list","name":"Test exception list","namespace_type":"single","os_types":[],"tags":[],"tie_breaker_id":"${jsonrule.tie_breaker_id}","type":"detection","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","version":1}\n{"exported_exception_list_count":1,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`; }; diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 27973854097db..ae04e20dfe86e 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -421,5 +421,5 @@ export const getEditedRule = (): CustomRule => ({ export const expectedExportedRule = (ruleResponse: Cypress.Response): string => { const jsonrule = ruleResponse.body; - return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-50000h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n`; + return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-50000h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n`; }; diff --git a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts index 1014835f81efe..c9ed5299c0336 100644 --- a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts @@ -24,7 +24,7 @@ export const SERVICE_NOW_CONNECTOR_CARD = '[data-test-subj=".servicenow-card"]'; export const TOASTER = '[data-test-subj="euiToastHeader"]'; -export const URL = '[data-test-subj="apiUrlFromInput"]'; +export const URL = '[data-test-subj="credentialsApiUrlFromInput"]'; export const USERNAME = '[data-test-subj="connector-servicenow-username-form-input"]'; diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx index eb606cd8ff583..8e972b92c2fa1 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx @@ -16,6 +16,7 @@ import { useSourcererScope, getScopeFromPath } from '../../../../common/containe import { TimelineId } from '../../../../../common/types/timeline'; import { AutoSaveWarningMsg } from '../../../../timelines/components/timeline/auto_save_warning'; import { Flyout } from '../../../../timelines/components/flyout'; +import { useResolveRedirect } from '../../../../common/hooks/use_resolve_redirect'; export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar'; @@ -26,6 +27,7 @@ export const SecuritySolutionBottomBar = React.memo( const [showTimeline] = useShowTimeline(); const { indicesExist } = useSourcererScope(getScopeFromPath(pathname)); + useResolveRedirect(); return indicesExist && showTimeline ? ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx index 281db88ebd057..2e04bbc5f1daf 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.tsx @@ -25,6 +25,7 @@ export const UseUrlStateMemo = React.memo( prevProps.pathName === nextProps.pathName && deepEqual(prevProps.urlState, nextProps.urlState) && deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + prevProps.search === nextProps.search && deepEqual(prevProps.navTabs, nextProps.navTabs) ); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index 4a448e9064090..f04cf30da61f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -124,7 +124,7 @@ export const useSetInitialStateFromUrl = () => { [dispatch, updateTimeline, updateTimelineIsLoading] ); - return setInitialStateFromUrl; + return Object.freeze({ setInitialStateFromUrl, updateTimeline, updateTimelineIsLoading }); }; const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/query_timeline_by_id_on_url_change.test.ts b/x-pack/plugins/security_solution/public/common/components/url_state/query_timeline_by_id_on_url_change.test.ts new file mode 100644 index 0000000000000..5cc4f8e8b80f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/url_state/query_timeline_by_id_on_url_change.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { queryTimelineById } from '../../../timelines/components/open_timeline/helpers'; +import { queryTimelineByIdOnUrlChange } from './query_timeline_by_id_on_url_change'; +import * as urlHelpers from './helpers'; + +jest.mock('../../../timelines/components/open_timeline/helpers'); + +describe('queryTimelineByIdOnUrlChange', () => { + const oldTestTimelineId = '04e8ffb0-2c2a-11ec-949c-39005af91f70'; + const newTestTimelineId = `${oldTestTimelineId}-newId`; + const oldTimelineRisonSearchString = `?timeline=(activeTab:query,graphEventId:%27%27,id:%27${oldTestTimelineId}%27,isOpen:!t)`; + const newTimelineRisonSearchString = `?timeline=(activeTab:query,graphEventId:%27%27,id:%27${newTestTimelineId}%27,isOpen:!t)`; + const mockUpdateTimeline = jest.fn(); + const mockUpdateTimelineIsLoading = jest.fn(); + const mockQueryTimelineById = jest.fn(); + beforeEach(() => { + (queryTimelineById as jest.Mock).mockImplementation(mockQueryTimelineById); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('when search strings are empty', () => { + it('should not call queryTimelineById', () => { + queryTimelineByIdOnUrlChange({ + oldSearch: '', + search: '', + timelineIdFromReduxStore: 'current-timeline-id', + updateTimeline: mockUpdateTimeline, + updateTimelineIsLoading: mockUpdateTimelineIsLoading, + }); + + expect(queryTimelineById).not.toBeCalled(); + }); + }); + + describe('when search string has not changed', () => { + it('should not call queryTimelineById', () => { + queryTimelineByIdOnUrlChange({ + oldSearch: oldTimelineRisonSearchString, + search: oldTimelineRisonSearchString, + timelineIdFromReduxStore: 'timeline-id', + updateTimeline: mockUpdateTimeline, + updateTimelineIsLoading: mockUpdateTimelineIsLoading, + }); + + expect(queryTimelineById).not.toBeCalled(); + }); + }); + + describe('when decode rison fails', () => { + it('should not call queryTimelineById', () => { + jest.spyOn(urlHelpers, 'decodeRisonUrlState').mockImplementationOnce(() => { + throw new Error('Unable to decode'); + }); + + queryTimelineByIdOnUrlChange({ + oldSearch: oldTimelineRisonSearchString, + search: newTimelineRisonSearchString, + timelineIdFromReduxStore: '', + updateTimeline: mockUpdateTimeline, + updateTimelineIsLoading: mockUpdateTimelineIsLoading, + }); + + expect(queryTimelineById).not.toBeCalled(); + }); + }); + + describe('when new id is not provided', () => { + it('should not call queryTimelineById', () => { + queryTimelineByIdOnUrlChange({ + oldSearch: oldTimelineRisonSearchString, + search: '?timeline=(activeTab:query)', // no id + timelineIdFromReduxStore: newTestTimelineId, + updateTimeline: mockUpdateTimeline, + updateTimelineIsLoading: mockUpdateTimelineIsLoading, + }); + + expect(queryTimelineById).not.toBeCalled(); + }); + }); + + describe('when new id matches the data in redux', () => { + it('should not call queryTimelineById', () => { + queryTimelineByIdOnUrlChange({ + oldSearch: oldTimelineRisonSearchString, + search: newTimelineRisonSearchString, + timelineIdFromReduxStore: newTestTimelineId, + updateTimeline: mockUpdateTimeline, + updateTimelineIsLoading: mockUpdateTimelineIsLoading, + }); + + expect(queryTimelineById).not.toBeCalled(); + }); + }); + + // You can only redirect or run into conflict scenarios when already viewing a timeline + describe('when not actively on a page with timeline in the search field', () => { + it('should not call queryTimelineById', () => { + queryTimelineByIdOnUrlChange({ + oldSearch: '?random=foo', + search: newTimelineRisonSearchString, + timelineIdFromReduxStore: oldTestTimelineId, + updateTimeline: mockUpdateTimeline, + updateTimelineIsLoading: mockUpdateTimelineIsLoading, + }); + + expect(queryTimelineById).not.toBeCalled(); + }); + }); + + describe('when an old timeline id exists, but a new id is given', () => { + it('should call queryTimelineById', () => { + queryTimelineByIdOnUrlChange({ + oldSearch: oldTimelineRisonSearchString, + search: newTimelineRisonSearchString, + timelineIdFromReduxStore: oldTestTimelineId, + updateTimeline: mockUpdateTimeline, + updateTimelineIsLoading: mockUpdateTimelineIsLoading, + }); + + expect(queryTimelineById).toBeCalledWith({ + activeTimelineTab: 'query', + duplicate: false, + graphEventId: '', + timelineId: newTestTimelineId, + openTimeline: true, + updateIsLoading: mockUpdateTimelineIsLoading, + updateTimeline: mockUpdateTimeline, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/query_timeline_by_id_on_url_change.ts b/x-pack/plugins/security_solution/public/common/components/url_state/query_timeline_by_id_on_url_change.ts new file mode 100644 index 0000000000000..2778cefdc7953 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/url_state/query_timeline_by_id_on_url_change.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Action } from 'typescript-fsa'; +import { DispatchUpdateTimeline } from '../../../timelines/components/open_timeline/types'; +import { queryTimelineById } from '../../../timelines/components/open_timeline/helpers'; +import { TimelineTabs } from '../../../../common/types/timeline'; +import { + decodeRisonUrlState, + getQueryStringFromLocation, + getParamFromQueryString, +} from './helpers'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { CONSTANTS } from './constants'; + +const getQueryStringKeyValue = ({ search, urlKey }: { search: string; urlKey: string }) => + getParamFromQueryString(getQueryStringFromLocation(search), urlKey); + +interface QueryTimelineIdOnUrlChange { + oldSearch?: string; + search: string; + timelineIdFromReduxStore: string; + updateTimeline: DispatchUpdateTimeline; + updateTimelineIsLoading: (status: { id: string; isLoading: boolean }) => Action<{ + id: string; + isLoading: boolean; + }>; +} + +/** + * After the initial load of the security solution, timeline is not updated when the timeline url search value is changed + * This is because those state changes happen in place and doesn't lead to a requerying of data for the new id. + * To circumvent this for the sake of the redirects needed for the saved object Id changes happening in 8.0 + * We are actively pulling the id changes that take place for timeline in the url and calling the query below + * to request the new data. + */ +export const queryTimelineByIdOnUrlChange = ({ + oldSearch, + search, + timelineIdFromReduxStore, + updateTimeline, + updateTimelineIsLoading, +}: QueryTimelineIdOnUrlChange) => { + const oldUrlStateString = getQueryStringKeyValue({ + urlKey: CONSTANTS.timeline, + search: oldSearch ?? '', + }); + + const newUrlStateString = getQueryStringKeyValue({ urlKey: CONSTANTS.timeline, search }); + + if (oldUrlStateString != null && newUrlStateString != null) { + let newTimeline = null; + let oldTimeline = null; + try { + newTimeline = decodeRisonUrlState(newUrlStateString); + } catch (error) { + // do nothing as timeline is defaulted to null + } + + try { + oldTimeline = decodeRisonUrlState(oldUrlStateString); + } catch (error) { + // do nothing as timeline is defaulted to null + } + const newId = newTimeline?.id; + const oldId = oldTimeline?.id; + + if (newId && newId !== oldId && newId !== timelineIdFromReduxStore) { + queryTimelineById({ + activeTimelineTab: newTimeline?.activeTab ?? TimelineTabs.query, + duplicate: false, + graphEventId: newTimeline?.graphEventId, + timelineId: newId, + openTimeline: true, + updateIsLoading: updateTimelineIsLoading, + updateTimeline, + }); + } + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index e803c091423be..06ed33ac69f6e 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -79,6 +79,7 @@ export interface PreviousLocationUrlState { pathName: string | undefined; pageName: string | undefined; urlState: UrlState; + search: string | undefined; } export interface UrlStateToRedux { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index bc47ba9d8ae99..3245d647227ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -39,6 +39,7 @@ import { } from './types'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { UrlInputsModel } from '../../store/inputs/model'; +import { queryTimelineByIdOnUrlChange } from './query_timeline_by_id_on_url_change'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -60,9 +61,10 @@ export const useUrlStateHooks = ({ const [isFirstPageLoad, setIsFirstPageLoad] = useState(true); const { filterManager, savedQueries } = useKibana().services.data.query; const { pathname: browserPathName } = useLocation(); - const prevProps = usePrevious({ pathName, pageName, urlState }); + const prevProps = usePrevious({ pathName, pageName, urlState, search }); - const setInitialStateFromUrl = useSetInitialStateFromUrl(); + const { setInitialStateFromUrl, updateTimeline, updateTimelineIsLoading } = + useSetInitialStateFromUrl(); const handleInitialize = useCallback( (type: UrlStateType) => { @@ -190,6 +192,16 @@ export const useUrlStateHooks = ({ document.title = `${getTitle(pageName, navTabs)} - Kibana`; }, [pageName, navTabs]); + useEffect(() => { + queryTimelineByIdOnUrlChange({ + oldSearch: prevProps.search, + search, + timelineIdFromReduxStore: urlState.timeline.id, + updateTimeline, + updateTimelineIsLoading, + }); + }, [search, prevProps.search, urlState.timeline.id, updateTimeline, updateTimelineIsLoading]); + return null; }; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.test.tsx new file mode 100644 index 0000000000000..bafbe078cdbdb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.test.tsx @@ -0,0 +1,166 @@ +/* + * 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 { useLocation } from 'react-router-dom'; +import { renderHook } from '@testing-library/react-hooks'; +import { useDeepEqualSelector } from './use_selector'; +import { useKibana } from '../lib/kibana'; +import { useResolveConflict } from './use_resolve_conflict'; +import * as urlHelpers from '../components/url_state/helpers'; + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useLocation: jest.fn(), + }; +}); +jest.mock('../lib/kibana'); +jest.mock('./use_selector'); +jest.mock('../../timelines/store/timeline/', () => ({ + timelineSelectors: { + getTimelineByIdSelector: () => jest.fn(), + }, +})); + +describe('useResolveConflict', () => { + const mockGetLegacyUrlConflict = jest.fn().mockReturnValue('Test!'); + beforeEach(() => { + jest.resetAllMocks(); + // Mock rison format in actual url + (useLocation as jest.Mock).mockReturnValue({ + pathname: 'my/cool/path', + search: + 'timeline=(activeTab:query,graphEventId:%27%27,id:%2704e8ffb0-2c2a-11ec-949c-39005af91f70%27,isOpen:!t)', + }); + (useKibana as jest.Mock).mockReturnValue({ + services: { + spaces: { + ui: { + components: { + getLegacyUrlConflict: mockGetLegacyUrlConflict, + }, + }, + }, + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('resolve object is not provided', () => { + it('should not show the conflict message', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + savedObjectId: 'current-saved-object-id', + activeTab: 'some-tab', + graphEventId: 'current-graph-event-id', + show: false, + })); + const { result } = renderHook<{}, JSX.Element | null>(() => useResolveConflict()); + expect(mockGetLegacyUrlConflict).not.toHaveBeenCalled(); + expect(result.current).toEqual(null); + }); + }); + + describe('outcome is exactMatch', () => { + it('should not show the conflict message', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'exactMatch', + }, + savedObjectId: 'current-saved-object-id', + activeTab: 'some-tab', + graphEventId: 'current-graph-event-id', + show: false, + })); + const { result } = renderHook<{}, JSX.Element | null>(() => useResolveConflict()); + expect(mockGetLegacyUrlConflict).not.toHaveBeenCalled(); + expect(result.current).toEqual(null); + }); + }); + + describe('outcome is aliasMatch', () => { + it('should not show the conflict message', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'aliasMatch', + alias_target_id: 'new-id', + }, + })); + const { result } = renderHook<{}, JSX.Element | null>(() => useResolveConflict()); + expect(mockGetLegacyUrlConflict).not.toHaveBeenCalled(); + expect(result.current).toEqual(null); + }); + }); + + describe('outcome is conflict', () => { + const mockTextContent = 'I am the visible conflict message'; + it('should show the conflict message', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'conflict', + alias_target_id: 'new-id', + }, + })); + mockGetLegacyUrlConflict.mockImplementation(() => mockTextContent); + const { result } = renderHook<{}, JSX.Element | null>(() => useResolveConflict()); + expect(mockGetLegacyUrlConflict).toHaveBeenCalledWith({ + objectNoun: 'timeline', + currentObjectId: '04e8ffb0-2c2a-11ec-949c-39005af91f70', + otherObjectId: 'new-id', + otherObjectPath: + 'my/cool/path?timeline=%28activeTab%3Aquery%2CgraphEventId%3A%27%27%2Cid%3Anew-id%2CisOpen%3A%21t%29', + }); + expect(result.current).toMatchInlineSnapshot(` + + I am the visible conflict message + + + `); + }); + + describe('rison is unable to be decoded', () => { + it('should use timeline values from redux to create the otherObjectPath', async () => { + jest.spyOn(urlHelpers, 'decodeRisonUrlState').mockImplementation(() => { + throw new Error('Unable to decode'); + }); + (useLocation as jest.Mock).mockReturnValue({ + pathname: 'my/cool/path', + search: '?foo=bar', + }); + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'conflict', + alias_target_id: 'new-id', + }, + savedObjectId: 'current-saved-object-id', + activeTab: 'some-tab', + graphEventId: 'current-graph-event-id', + show: false, + })); + mockGetLegacyUrlConflict.mockImplementation(() => mockTextContent); + renderHook(() => useResolveConflict()); + const { result } = renderHook<{}, JSX.Element | null>(() => useResolveConflict()); + expect(mockGetLegacyUrlConflict).toHaveBeenCalledWith({ + objectNoun: 'timeline', + currentObjectId: 'current-saved-object-id', + otherObjectId: 'new-id', + otherObjectPath: + 'my/cool/path?foo=bar&timeline=%28activeTab%3Asome-tab%2CgraphEventId%3Acurrent-graph-event-id%2Cid%3Anew-id%2CisOpen%3A%21f%29', + }); + expect(result.current).toMatchInlineSnapshot(` + + I am the visible conflict message + + + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx new file mode 100644 index 0000000000000..6a493d944ecda --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx @@ -0,0 +1,100 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { EuiSpacer } from '@elastic/eui'; +import { useDeepEqualSelector } from './use_selector'; +import { TimelineId } from '../../../common/types/timeline'; +import { timelineSelectors } from '../../timelines/store/timeline'; +import { TimelineUrl } from '../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../timelines/store/timeline/defaults'; +import { decodeRisonUrlState, encodeRisonUrlState } from '../components/url_state/helpers'; +import { useKibana } from '../lib/kibana'; +import { CONSTANTS } from '../components/url_state/constants'; + +/** + * Unfortunately the url change initiated when clicking the button to otherObjectPath doesn't seem to be + * respected by the useSetInitialStateFromUrl here: x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx + * + * FYI: It looks like the routing causes replaceStateInLocation to be called instead: + * x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts + * + * Potentially why the markdown component needs a click handler as well for timeline? + * see: /x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx + */ +export const useResolveConflict = () => { + const { search, pathname } = useLocation(); + const { spaces } = useKibana().services; + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { resolveTimelineConfig, savedObjectId, show, graphEventId, activeTab } = + useDeepEqualSelector((state) => getTimeline(state, TimelineId.active) ?? timelineDefaults); + + const getLegacyUrlConflictCallout = useCallback(() => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if ( + !spaces || + resolveTimelineConfig?.outcome !== 'conflict' || + resolveTimelineConfig?.alias_target_id == null + ) { + return null; + } + + const searchQuery = new URLSearchParams(search); + const timelineRison = searchQuery.get(CONSTANTS.timeline) ?? undefined; + // Try to get state on URL, but default to what's in Redux in case of decodeRisonFailure + const currentTimelineState = { + id: savedObjectId ?? '', + isOpen: !!show, + activeTab, + graphEventId, + }; + let timelineSearch: TimelineUrl = currentTimelineState; + try { + timelineSearch = decodeRisonUrlState(timelineRison) ?? currentTimelineState; + } catch (error) { + // do nothing as it's already defaulted on line 77 + } + // 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 currentObjectId = timelineSearch?.id; + const newSavedObjectId = resolveTimelineConfig?.alias_target_id ?? ''; // This is always defined if outcome === 'conflict' + + const newTimelineSearch: TimelineUrl = { + ...timelineSearch, + id: newSavedObjectId, + }; + const newTimelineRison = encodeRisonUrlState(newTimelineSearch); + searchQuery.set(CONSTANTS.timeline, newTimelineRison); + + const newPath = `${pathname}?${searchQuery.toString()}${window.location.hash}`; + + return ( + <> + {spaces.ui.components.getLegacyUrlConflict({ + objectNoun: CONSTANTS.timeline, + currentObjectId, + otherObjectId: newSavedObjectId, + otherObjectPath: newPath, + })} + + + ); + }, [ + activeTab, + graphEventId, + pathname, + resolveTimelineConfig?.alias_target_id, + resolveTimelineConfig?.outcome, + savedObjectId, + search, + show, + spaces, + ]); + + return useMemo(() => getLegacyUrlConflictCallout(), [getLegacyUrlConflictCallout]); +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_resolve_redirect.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_redirect.test.ts new file mode 100644 index 0000000000000..c9a0eedefd0af --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_redirect.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { useLocation } from 'react-router-dom'; +import { renderHook } from '@testing-library/react-hooks'; +import { useDeepEqualSelector } from './use_selector'; +import { useKibana } from '../lib/kibana'; +import { useResolveRedirect } from './use_resolve_redirect'; +import * as urlHelpers from '../components/url_state/helpers'; + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useLocation: jest.fn(), + }; +}); +jest.mock('../lib/kibana'); +jest.mock('./use_selector'); +jest.mock('../../timelines/store/timeline/', () => ({ + timelineSelectors: { + getTimelineByIdSelector: () => jest.fn(), + }, +})); + +describe('useResolveRedirect', () => { + const mockRedirectLegacyUrl = jest.fn(); + beforeEach(() => { + jest.resetAllMocks(); + // Mock rison format in actual url + (useLocation as jest.Mock).mockReturnValue({ + pathname: 'my/cool/path', + search: + 'timeline=(activeTab:query,graphEventId:%27%27,id:%2704e8ffb0-2c2a-11ec-949c-39005af91f70%27,isOpen:!t)', + }); + (useKibana as jest.Mock).mockReturnValue({ + services: { + spaces: { + ui: { + redirectLegacyUrl: mockRedirectLegacyUrl, + }, + }, + }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('resolve object is not provided', () => { + it('should not redirect', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + savedObjectId: 'current-saved-object-id', + activeTab: 'some-tab', + graphEventId: 'current-graph-event-id', + show: false, + })); + renderHook(() => useResolveRedirect()); + expect(mockRedirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); + + describe('outcome is exactMatch', () => { + it('should not redirect', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'exactMatch', + }, + savedObjectId: 'current-saved-object-id', + activeTab: 'some-tab', + graphEventId: 'current-graph-event-id', + show: false, + })); + renderHook(() => useResolveRedirect()); + expect(mockRedirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); + + describe('outcome is aliasMatch', () => { + it('should redirect to url with id:new-id if outcome is aliasMatch', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'aliasMatch', + alias_target_id: 'new-id', + }, + })); + renderHook(() => useResolveRedirect()); + expect(mockRedirectLegacyUrl).toHaveBeenCalledWith( + 'my/cool/path?timeline=%28activeTab%3Aquery%2CgraphEventId%3A%27%27%2Cid%3Anew-id%2CisOpen%3A%21t%29', + 'timeline' + ); + }); + + describe('rison is unable to be decoded', () => { + it('should use timeline values from redux to create the redirect path', async () => { + jest.spyOn(urlHelpers, 'decodeRisonUrlState').mockImplementation(() => { + throw new Error('Unable to decode'); + }); + (useLocation as jest.Mock).mockReturnValue({ + pathname: 'my/cool/path', + search: '?foo=bar', + }); + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'aliasMatch', + alias_target_id: 'new-id', + }, + savedObjectId: 'current-saved-object-id', + activeTab: 'some-tab', + graphEventId: 'current-graph-event-id', + show: false, + })); + renderHook(() => useResolveRedirect()); + expect(mockRedirectLegacyUrl).toHaveBeenCalledWith( + 'my/cool/path?foo=bar&timeline=%28activeTab%3Asome-tab%2CgraphEventId%3Acurrent-graph-event-id%2Cid%3Anew-id%2CisOpen%3A%21f%29', + 'timeline' + ); + }); + }); + }); + + describe('outcome is conflict', () => { + it('should not redirect', async () => { + (useDeepEqualSelector as jest.Mock).mockImplementation(() => ({ + resolveTimelineConfig: { + outcome: 'conflict', + alias_target_id: 'new-id', + }, + })); + renderHook(() => useResolveRedirect()); + expect(mockRedirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_resolve_redirect.ts b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_redirect.ts new file mode 100644 index 0000000000000..a6ba0b24828e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_resolve_redirect.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useDeepEqualSelector } from './use_selector'; +import { TimelineId } from '../../../common/types/timeline'; +import { timelineSelectors } from '../../timelines/store/timeline/'; +import { timelineDefaults } from '../../timelines/store/timeline/defaults'; +import { decodeRisonUrlState, encodeRisonUrlState } from '../components/url_state/helpers'; +import { useKibana } from '../lib/kibana'; +import { TimelineUrl } from '../../timelines/store/timeline/model'; +import { CONSTANTS } from '../components/url_state/constants'; + +/** + * This hooks is specifically for use with the resolve api that was introduced as part of 7.16 + * If a deep link id has been migrated to a new id, this hook will cause a redirect to a url with + * the new ID. + */ + +export const useResolveRedirect = () => { + const { search, pathname } = useLocation(); + const [hasRedirected, updateHasRedirected] = useState(false); + const { spaces } = useKibana().services; + + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const { resolveTimelineConfig, savedObjectId, show, activeTab, graphEventId } = + useDeepEqualSelector((state) => getTimeline(state, TimelineId.active) ?? timelineDefaults); + + const redirect = useCallback(() => { + const searchQuery = new URLSearchParams(search); + const timelineRison = searchQuery.get(CONSTANTS.timeline) ?? undefined; + + // Try to get state on URL, but default to what's in Redux in case of decodeRisonFailure + const currentTimelineState = { + id: savedObjectId ?? '', + isOpen: !!show, + activeTab, + graphEventId, + }; + let timelineSearch: TimelineUrl = currentTimelineState; + try { + timelineSearch = decodeRisonUrlState(timelineRison) ?? currentTimelineState; + } catch (error) { + // do nothing as it's already defaulted on line 77 + } + + if ( + hasRedirected || + !spaces || + resolveTimelineConfig?.outcome !== 'aliasMatch' || + resolveTimelineConfig?.alias_target_id == null + ) { + return null; + } + + // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash + const newObjectId = resolveTimelineConfig?.alias_target_id ?? ''; // This is always defined if outcome === 'aliasMatch' + const newTimelineSearch = { + ...timelineSearch, + id: newObjectId, + }; + const newTimelineRison = encodeRisonUrlState(newTimelineSearch); + searchQuery.set(CONSTANTS.timeline, newTimelineRison); + const newPath = `${pathname}?${searchQuery.toString()}`; + spaces.ui.redirectLegacyUrl(newPath, CONSTANTS.timeline); + // Prevent the effect from being called again as the url change takes place in location rather than a true redirect + updateHasRedirected(true); + }, [ + activeTab, + graphEventId, + hasRedirected, + pathname, + resolveTimelineConfig?.outcome, + resolveTimelineConfig?.alias_target_id, + savedObjectId, + search, + show, + spaces, + ]); + + useEffect(() => { + redirect(); + }, [redirect]); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts index f1e1c42539eff..2521d14481ca8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts @@ -9,7 +9,7 @@ import { TimelineStatus, TimelineType } from '../../../../../common/types/timeli export const mockTimeline = { data: { - getOneTimeline: { + timeline: { savedObjectId: 'eb2781c0-1df5-11eb-8589-2f13958b79f7', columns: [ { @@ -163,6 +163,7 @@ export const mockTimeline = { version: 'WzQ4NSwxXQ==', __typename: 'TimelineResult', }, + outcome: 'exactMatch', }, loading: false, networkStatus: 7, @@ -171,7 +172,7 @@ export const mockTimeline = { export const mockTemplate = { data: { - getOneTimeline: { + timeline: { savedObjectId: '0c70a200-1de0-11eb-885c-6fc13fca1850', columns: [ { @@ -416,6 +417,7 @@ export const mockTemplate = { version: 'WzQwMywxXQ==', __typename: 'TimelineResult', }, + outcome: 'exactMatch', }, loading: false, networkStatus: 7, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 5d52d2c8a4d48..1b93f1556a95c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -50,7 +50,7 @@ import { mockTimeline as mockSelectedTimeline, mockTemplate as mockSelectedTemplate, } from './__mocks__'; -import { getTimeline } from '../../containers/api'; +import { resolveTimeline } from '../../containers/api'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; jest.mock('../../../common/store/inputs/actions'); @@ -951,7 +951,7 @@ describe('helpers', () => { }; beforeAll(async () => { - (getTimeline as jest.Mock).mockRejectedValue(mockError); + (resolveTimeline as jest.Mock).mockRejectedValue(mockError); queryTimelineById<{}>(args as unknown as QueryTimelineById<{}>); }); @@ -986,7 +986,7 @@ describe('helpers', () => { }; beforeAll(async () => { - (getTimeline as jest.Mock).mockResolvedValue(selectedTimeline); + (resolveTimeline as jest.Mock).mockResolvedValue(selectedTimeline); await queryTimelineById<{}>(args as unknown as QueryTimelineById<{}>); }); @@ -1002,7 +1002,7 @@ describe('helpers', () => { }); test('get timeline by Id', () => { - expect(getTimeline).toHaveBeenCalled(); + expect(resolveTimeline).toHaveBeenCalled(); }); test('it does not call onError when an error does not occur', () => { @@ -1011,7 +1011,7 @@ describe('helpers', () => { test('Do not override daterange if TimelineStatus is active', () => { const { timeline } = formatTimelineResultToModel( - omitTypenameInTimeline(getOr({}, 'data.getOneTimeline', selectedTimeline)), + omitTypenameInTimeline(getOr({}, 'data.timeline', selectedTimeline)), args.duplicate, args.timelineType ); @@ -1044,7 +1044,7 @@ describe('helpers', () => { }; beforeAll(async () => { - (getTimeline as jest.Mock).mockResolvedValue(selectedTimeline); + (resolveTimeline as jest.Mock).mockResolvedValue(selectedTimeline); await queryTimelineById<{}>(args as unknown as QueryTimelineById<{}>); }); @@ -1060,12 +1060,12 @@ describe('helpers', () => { }); test('get timeline by Id', () => { - expect(getTimeline).toHaveBeenCalled(); + expect(resolveTimeline).toHaveBeenCalled(); }); test('should not override daterange if TimelineStatus is active', () => { const { timeline } = formatTimelineResultToModel( - omitTypenameInTimeline(getOr({}, 'data.getOneTimeline', selectedTimeline)), + omitTypenameInTimeline(getOr({}, 'data.timeline', selectedTimeline)), args.duplicate, args.timelineType ); @@ -1085,6 +1085,10 @@ describe('helpers', () => { to: '2020-07-08T08:20:18.966Z', notes: [], id: TimelineId.active, + resolveTimelineConfig: { + outcome: 'exactMatch', + alias_target_id: undefined, + }, }); }); @@ -1112,12 +1116,12 @@ describe('helpers', () => { }; beforeAll(async () => { - (getTimeline as jest.Mock).mockResolvedValue(template); + (resolveTimeline as jest.Mock).mockResolvedValue(template); await queryTimelineById<{}>(args as unknown as QueryTimelineById<{}>); }); afterAll(() => { - (getTimeline as jest.Mock).mockReset(); + (resolveTimeline as jest.Mock).mockReset(); jest.clearAllMocks(); }); @@ -1129,12 +1133,12 @@ describe('helpers', () => { }); test('get timeline by Id', () => { - expect(getTimeline).toHaveBeenCalled(); + expect(resolveTimeline).toHaveBeenCalled(); }); test('override daterange if TimelineStatus is immutable', () => { const { timeline } = formatTimelineResultToModel( - omitTypenameInTimeline(getOr({}, 'data.getOneTimeline', template)), + omitTypenameInTimeline(getOr({}, 'data.timeline', template)), args.duplicate, args.timelineType ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 1fbddf61f8cd3..f325ab34e88d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -20,6 +20,7 @@ import { TimelineType, TimelineTabs, TimelineResult, + SingleTimelineResolveResponse, ColumnHeaderResult, FilterTimelineResult, DataProviderResult, @@ -65,7 +66,7 @@ import { DEFAULT_FROM_MOMENT, DEFAULT_TO_MOMENT, } from '../../../common/utils/default_date_settings'; -import { getTimeline } from '../../containers/api'; +import { resolveTimeline } from '../../containers/api'; import { PinnedEvent } from '../../../../common/types/timeline/pinned_event'; import { NoteResult } from '../../../../common/types/timeline/note'; @@ -346,11 +347,12 @@ export const queryTimelineById = ({ updateTimeline, }: QueryTimelineById) => { updateIsLoading({ id: TimelineId.active, isLoading: true }); - Promise.resolve(getTimeline(timelineId)) + Promise.resolve(resolveTimeline(timelineId)) .then((result) => { - const timelineToOpen: TimelineResult = omitTypenameInTimeline( - getOr({}, 'data.getOneTimeline', result) - ); + const data: SingleTimelineResolveResponse['data'] | null = getOr(null, 'data', result); + if (!data) return; + + const timelineToOpen = omitTypenameInTimeline(data.timeline); const { timeline, notes } = formatTimelineResultToModel( timelineToOpen, @@ -370,6 +372,10 @@ export const queryTimelineById = ({ from, id: TimelineId.active, notes, + resolveTimelineConfig: { + outcome: data.outcome, + alias_target_id: data.alias_target_id, + }, timeline: { ...timeline, activeTab: activeTimelineTab, @@ -399,6 +405,7 @@ export const dispatchUpdateTimeline = forceNotes = false, from, notes, + resolveTimelineConfig, timeline, to, ruleNote, @@ -429,7 +436,9 @@ export const dispatchUpdateTimeline = } else { dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); } - dispatch(dispatchAddTimeline({ id, timeline, savedTimeline: duplicate })); + dispatch( + dispatchAddTimeline({ id, timeline, resolveTimelineConfig, savedTimeline: duplicate }) + ); if ( timeline.kqlQuery != null && timeline.kqlQuery.filterQuery != null && diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index 79a700856c00f..4c9ce991252dc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -16,6 +16,7 @@ import { TemplateTimelineTypeLiteral, RowRendererId, TimelineStatusLiteralWithNull, + SingleTimelineResolveResponse, } from '../../../../common/types/timeline'; /** The users who added a timeline to favorites */ @@ -194,12 +195,17 @@ export interface OpenTimelineProps { hideActions?: ActionTimelineToShow[]; } +export interface ResolveTimelineConfig { + alias_target_id: SingleTimelineResolveResponse['data']['alias_target_id']; + outcome: SingleTimelineResolveResponse['data']['outcome']; +} export interface UpdateTimeline { duplicate: boolean; id: string; forceNotes?: boolean; from: string; notes: NoteResult[] | null | undefined; + resolveTimelineConfig?: ResolveTimelineConfig; timeline: TimelineModel; to: string; ruleNote?: string; @@ -210,6 +216,7 @@ export type DispatchUpdateTimeline = ({ id, from, notes, + resolveTimelineConfig, timeline, to, ruleNote, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index db7a3cc3c9900..c91673e5f931c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -40,6 +40,12 @@ const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); +jest.mock('../../../common/hooks/use_resolve_conflict', () => { + return { + useResolveConflict: jest.fn().mockImplementation(() => null), + }; +}); + jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index a199dd5aa55f8..ca883529b5ce6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -28,6 +28,7 @@ import { TabsContent } from './tabs_content'; import { HideShowContainer, TimelineContainer } from './styles'; import { useTimelineFullScreen } from '../../../common/containers/use_full_screen'; import { EXIT_FULL_SCREEN_CLASS_NAME } from '../../../common/components/exit_full_screen'; +import { useResolveConflict } from '../../../common/hooks/use_resolve_conflict'; const TimelineTemplateBadge = styled.div` background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; @@ -119,6 +120,7 @@ const StatefulTimelineComponent: React.FC = ({ [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] ); const timelineContext = useMemo(() => ({ timelineId }), [timelineId]); + const resolveConflictComponent = useResolveConflict(); return ( @@ -132,7 +134,7 @@ const StatefulTimelineComponent: React.FC = ({ {timelineType === TimelineType.template && ( {i18n.TIMELINE_TEMPLATE} )} - + {resolveConflictComponent} span { + .euiTab__content { display: flex; flex-direction: row; white-space: pre; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index 7f74912be09b4..44a750cc7283b 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -338,19 +338,6 @@ export const getTimelineTemplate = async (templateTimelineId: string) => { return decodeSingleTimelineResponse(response); }; -export const getResolvedTimelineTemplate = async (templateTimelineId: string) => { - const response = await KibanaServices.get().http.get( - TIMELINE_RESOLVE_URL, - { - query: { - template_timeline_id: templateTimelineId, - }, - } - ); - - return decodeResolvedSingleTimelineResponse(response); -}; - export const getAllTimelines = async (args: GetTimelinesArgs, abortSignal: AbortSignal) => { const response = await KibanaServices.get().http.fetch(TIMELINES_URL, { method: 'GET', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 95ad6c5d44ca3..f3a70bd1390ae 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -25,6 +25,7 @@ import type { SerializedFilterQuery, } from '../../../../common/types/timeline'; import { tGridActions } from '../../../../../timelines/public'; +import { ResolveTimelineConfig } from '../../components/open_timeline/types'; export const { applyDeltaToColumnWidth, clearEventsDeleted, @@ -91,6 +92,7 @@ export const updateTimeline = actionCreator<{ export const addTimeline = actionCreator<{ id: string; timeline: TimelineModel; + resolveTimelineConfig?: ResolveTimelineConfig; savedTimeline?: boolean; }>('ADD_TIMELINE'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 4691872bfb927..4c2b8d2992d3d 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -14,64 +14,65 @@ import { SubsetTimelineModel, TimelineModel } from './model'; // normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false); -export const timelineDefaults: SubsetTimelineModel & Pick = - { - activeTab: TimelineTabs.query, - prevActiveTab: TimelineTabs.query, - columns: defaultHeaders, - documentType: '', - defaultColumns: defaultHeaders, - dataProviders: [], - dateRange: { start, end }, - deletedEventIds: [], - description: '', - eqlOptions: { - eventCategoryField: 'event.category', - tiebreakerField: '', - timestampField: '@timestamp', - query: '', - size: 100, +export const timelineDefaults: SubsetTimelineModel & + Pick = { + activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, + columns: defaultHeaders, + documentType: '', + defaultColumns: defaultHeaders, + dataProviders: [], + dateRange: { start, end }, + deletedEventIds: [], + description: '', + eqlOptions: { + eventCategoryField: 'event.category', + tiebreakerField: '', + timestampField: '@timestamp', + query: '', + size: 100, + }, + eventType: 'all', + eventIdToNoteIds: {}, + excludedRowRendererIds: [], + expandedDetail: {}, + highlightedDropAndProviderId: '', + historyIds: [], + filters: [], + indexNames: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + }, + loadingEventIds: [], + resolveTimelineConfig: undefined, + queryFields: [], + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: null, + selectAll: false, + selectedEventIds: {}, + show: false, + showCheckboxes: false, + sort: [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: 'desc', }, - eventType: 'all', - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - expandedDetail: {}, - highlightedDropAndProviderId: '', - historyIds: [], - filters: [], - indexNames: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - isSaving: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - }, - loadingEventIds: [], - queryFields: [], - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - savedObjectId: null, - selectAll: false, - selectedEventIds: {}, - show: false, - showCheckboxes: false, - sort: [ - { - columnId: '@timestamp', - columnType: 'number', - sortDirection: 'desc', - }, - ], - status: TimelineStatus.draft, - version: null, - }; + ], + status: TimelineStatus.draft, + version: null, +}; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 6ee844958aeed..b7af561ae2a04 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -47,6 +47,7 @@ import { RESIZED_COLUMN_MIN_WITH, } from '../../components/timeline/body/constants'; import { activeTimeline } from '../../containers/active_timeline_context'; +import { ResolveTimelineConfig } from '../../components/open_timeline/types'; export const isNotNull = (value: T | null): value is T => value !== null; @@ -124,6 +125,7 @@ export const addTimelineNoteToEvent = ({ interface AddTimelineParams { id: string; + resolveTimelineConfig?: ResolveTimelineConfig; timeline: TimelineModel; timelineById: TimelineById; } @@ -145,6 +147,7 @@ export const shouldResetActiveTimelineContext = ( */ export const addTimelineToStore = ({ id, + resolveTimelineConfig, timeline, timelineById, }: AddTimelineParams): TimelineById => { @@ -159,6 +162,7 @@ export const addTimelineToStore = ({ filterManager: timelineById[id].filterManager, isLoading: timelineById[id].isLoading, initialized: timelineById[id].initialized, + resolveTimelineConfig, dateRange: timeline.status === TimelineStatus.immutable && timeline.timelineType === TimelineType.template diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index b53da997c08cb..29b49197ef797 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -15,6 +15,7 @@ import type { } from '../../../../common/types/timeline'; import { PinnedEvent } from '../../../../common/types/timeline/pinned_event'; import type { TGridModelForTimeline } from '../../../../../timelines/public'; +import { ResolveTimelineConfig } from '../../components/open_timeline/types'; export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; @@ -59,6 +60,7 @@ export type TimelineModel = TGridModelForTimeline & { /** Events pinned to this timeline */ pinnedEventIds: Record; pinnedEventsSaveObject: Record; + resolveTimelineConfig?: ResolveTimelineConfig; showSaveModal?: boolean; savedQueryId?: string | null; /** When true, show the timeline flyover */ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 97fa72667a3c6..e997bbd848d50 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -94,9 +94,14 @@ export const initialTimelineState: TimelineState = { /** The reducer for all timeline actions */ export const timelineReducer = reducerWithInitialState(initialTimelineState) - .case(addTimeline, (state, { id, timeline }) => ({ + .case(addTimeline, (state, { id, timeline, resolveTimelineConfig }) => ({ ...state, - timelineById: addTimelineToStore({ id, timeline, timelineById: state.timelineById }), + timelineById: addTimelineToStore({ + id, + timeline, + resolveTimelineConfig, + timelineById: state.timelineById, + }), })) .case(createTimeline, (state, { id, timelineType = TimelineType.default, ...timelineProps }) => { return { diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index bee0e9b3a3d1d..61813d1a122b4 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -9,7 +9,6 @@ import { CoreStart } from '../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; -import { SpacesPluginStart } from '../../../plugins/spaces/public'; import { LensPublicStart } from '../../../plugins/lens/public'; import { NewsfeedPublicPluginStart } from '../../../../src/plugins/newsfeed/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; @@ -18,6 +17,7 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/p import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { FleetStart } from '../../fleet/public'; import { PluginStart as ListsPluginStart } from '../../lists/public'; +import { SpacesPluginStart } from '../../spaces/public'; import { TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, diff --git a/x-pack/plugins/security_solution/server/config.mock.ts b/x-pack/plugins/security_solution/server/config.mock.ts new file mode 100644 index 0000000000000..c1d1e02ca35f4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/config.mock.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_SIGNALS_INDEX, SIGNALS_INDEX_KEY } from '../common/constants'; +import { + ExperimentalFeatures, + parseExperimentalConfigValue, +} from '../common/experimental_features'; +import { ConfigType } from './config'; +import { UnderlyingLogClient } from './lib/detection_engine/rule_execution_log/types'; + +export const createMockConfig = (): ConfigType => { + const enableExperimental: string[] = []; + + return { + [SIGNALS_INDEX_KEY]: DEFAULT_SIGNALS_INDEX, + maxRuleImportExportSize: 10000, + maxRuleImportPayloadBytes: 10485760, + maxTimelineImportExportSize: 10000, + maxTimelineImportPayloadBytes: 10485760, + enableExperimental, + endpointResultListDefaultFirstPageIndex: 0, + endpointResultListDefaultPageSize: 10, + packagerTaskInterval: '60s', + alertMergeStrategy: 'missingFields', + alertIgnoreFields: [], + prebuiltRulesFromFileSystem: true, + prebuiltRulesFromSavedObjects: false, + ruleExecutionLog: { + underlyingClient: UnderlyingLogClient.savedObjects, + }, + + kibanaIndex: '.kibana', + experimentalFeatures: parseExperimentalConfigValue(enableExperimental), + }; +}; + +const withExperimentalFeature = ( + config: ConfigType, + feature: keyof ExperimentalFeatures +): ConfigType => { + const enableExperimental = config.enableExperimental.concat(feature); + return { + ...config, + enableExperimental, + experimentalFeatures: parseExperimentalConfigValue(enableExperimental), + }; +}; + +const withRuleRegistryEnabled = (config: ConfigType, isEnabled: boolean): ConfigType => { + return isEnabled ? withExperimentalFeature(config, 'ruleRegistryEnabled') : config; +}; + +export const configMock = { + createDefault: createMockConfig, + withExperimentalFeature, + withRuleRegistryEnabled, +}; diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 61cbb5641c5f6..072e23b7a773c 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -9,8 +9,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX } from '../common/constants'; import { + ExperimentalFeatures, getExperimentalAllowedValues, isValidExperimentalValue, + parseExperimentalConfigValue, } from '../common/experimental_features'; import { UnderlyingLogClient } from './lib/detection_engine/rule_execution_log/types'; @@ -134,7 +136,23 @@ export const configSchema = schema.object({ prebuiltRulesFromSavedObjects: schema.boolean({ defaultValue: true }), }); -export const createConfig = (context: PluginInitializerContext) => - context.config.get>(); +export type ConfigSchema = TypeOf; -export type ConfigType = TypeOf; +export type ConfigType = ConfigSchema & { + kibanaIndex: string; + experimentalFeatures: ExperimentalFeatures; +}; + +export const createConfig = (context: PluginInitializerContext): ConfigType => { + const globalConfig = context.config.legacy.get(); + const pluginConfig = context.config.get>(); + + const kibanaIndex = globalConfig.kibana.index; + const experimentalFeatures = parseExperimentalConfigValue(pluginConfig.enableExperimental); + + return { + ...pluginConfig, + kibanaIndex, + experimentalFeatures, + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 5a47c8a616c00..caea18da75ae4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -27,13 +27,18 @@ import { import { ManifestManager } from './services/artifacts'; import { AppClientFactory } from '../client'; import { ConfigType } from '../config'; +import { IRequestContextFactory } from '../request_context_factory'; import { LicenseService } from '../../common/license'; -import { - ExperimentalFeatures, - parseExperimentalConfigValue, -} from '../../common/experimental_features'; +import { ExperimentalFeatures } from '../../common/experimental_features'; import { EndpointMetadataService } from './services/metadata'; -import { EndpointAppContentServicesNotStartedError } from './errors'; +import { + EndpointAppContentServicesNotSetUpError, + EndpointAppContentServicesNotStartedError, +} from './errors'; + +export interface EndpointAppContextServiceSetupContract { + securitySolutionRequestContextFactory: IRequestContextFactory; +} export type EndpointAppContextServiceStartContract = Partial< Pick< @@ -59,40 +64,29 @@ export type EndpointAppContextServiceStartContract = Partial< * of the plugin lifecycle. And stop during the stop phase, if needed. */ export class EndpointAppContextService { - private agentService: AgentService | undefined; - private manifestManager: ManifestManager | undefined; - private packagePolicyService: PackagePolicyServiceInterface | undefined; - private agentPolicyService: AgentPolicyServiceInterface | undefined; - private config: ConfigType | undefined; - private license: LicenseService | undefined; + private setupDependencies: EndpointAppContextServiceSetupContract | null = null; + private startDependencies: EndpointAppContextServiceStartContract | null = null; public security: SecurityPluginStart | undefined; - private cases: CasesPluginStartContract | undefined; - private endpointMetadataService: EndpointMetadataService | undefined; - private experimentalFeatures: ExperimentalFeatures | undefined; + + public setup(dependencies: EndpointAppContextServiceSetupContract) { + this.setupDependencies = dependencies; + } public start(dependencies: EndpointAppContextServiceStartContract) { - this.agentService = dependencies.agentService; - this.packagePolicyService = dependencies.packagePolicyService; - this.agentPolicyService = dependencies.agentPolicyService; - this.manifestManager = dependencies.manifestManager; - this.config = dependencies.config; - this.license = dependencies.licenseService; + if (!this.setupDependencies) { + throw new EndpointAppContentServicesNotSetUpError(); + } + + this.startDependencies = dependencies; this.security = dependencies.security; - this.cases = dependencies.cases; - this.endpointMetadataService = dependencies.endpointMetadataService; - this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental); - if (this.manifestManager && dependencies.registerIngestCallback) { + if (dependencies.registerIngestCallback && dependencies.manifestManager) { dependencies.registerIngestCallback( 'packagePolicyCreate', getPackagePolicyCreateCallback( dependencies.logger, - this.manifestManager, - dependencies.appClientFactory, - dependencies.config.maxTimelineImportExportSize, - dependencies.config.prebuiltRulesFromFileSystem, - dependencies.config.prebuiltRulesFromSavedObjects, - dependencies.security, + dependencies.manifestManager, + this.setupDependencies.securitySolutionRequestContextFactory, dependencies.alerting, dependencies.licenseService, dependencies.exceptionListsClient @@ -106,7 +100,10 @@ export class EndpointAppContextService { dependencies.registerIngestCallback( 'postPackagePolicyDelete', - getPackagePolicyDeleteCallback(dependencies.exceptionListsClient, this.experimentalFeatures) + getPackagePolicyDeleteCallback( + dependencies.exceptionListsClient, + dependencies.config.experimentalFeatures + ) ); } } @@ -114,43 +111,43 @@ export class EndpointAppContextService { public stop() {} public getExperimentalFeatures(): Readonly | undefined { - return this.experimentalFeatures; + return this.startDependencies?.config.experimentalFeatures; } public getEndpointMetadataService(): EndpointMetadataService { - if (!this.endpointMetadataService) { + if (this.startDependencies == null) { throw new EndpointAppContentServicesNotStartedError(); } - return this.endpointMetadataService; + return this.startDependencies.endpointMetadataService; } public getAgentService(): AgentService | undefined { - return this.agentService; + return this.startDependencies?.agentService; } public getPackagePolicyService(): PackagePolicyServiceInterface | undefined { - return this.packagePolicyService; + return this.startDependencies?.packagePolicyService; } public getAgentPolicyService(): AgentPolicyServiceInterface | undefined { - return this.agentPolicyService; + return this.startDependencies?.agentPolicyService; } public getManifestManager(): ManifestManager | undefined { - return this.manifestManager; + return this.startDependencies?.manifestManager; } public getLicenseService(): LicenseService { - if (!this.license) { + if (this.startDependencies == null) { throw new EndpointAppContentServicesNotStartedError(); } - return this.license; + return this.startDependencies.licenseService; } public async getCasesClient(req: KibanaRequest): Promise { - if (!this.cases) { + if (this.startDependencies?.cases == null) { throw new EndpointAppContentServicesNotStartedError(); } - return this.cases.getCasesClientWithRequest(req); + return this.startDependencies.cases.getCasesClientWithRequest(req); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/errors.ts b/x-pack/plugins/security_solution/server/endpoint/errors.ts index fae15984d9c44..7260d6055b310 100644 --- a/x-pack/plugins/security_solution/server/endpoint/errors.ts +++ b/x-pack/plugins/security_solution/server/endpoint/errors.ts @@ -17,6 +17,12 @@ export class EndpointError extends Error { export class NotFoundError extends EndpointError {} +export class EndpointAppContentServicesNotSetUpError extends EndpointError { + constructor() { + super('EndpointAppContextService has not been set up (EndpointAppContextService.setup())'); + } +} + export class EndpointAppContentServicesNotStartedError extends EndpointError { constructor() { super('EndpointAppContextService has not been started (EndpointAppContextService.start())'); diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 39833b6e995d1..190770f3d860d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -22,6 +22,7 @@ import { AppClientFactory } from '../client'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService, + EndpointAppContextServiceSetupContract, EndpointAppContextServiceStartContract, } from './endpoint_app_context_services'; import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager'; @@ -37,6 +38,7 @@ import { parseExperimentalConfigValue } from '../../common/experimental_features // a restricted path. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { createCasesClientMock } from '../../../cases/server/client/mocks'; +import { requestContextFactoryMock } from '../request_context_factory.mock'; import { EndpointMetadataService } from './services/metadata'; /** @@ -69,13 +71,25 @@ export const createMockEndpointAppContextService = ( } as unknown as jest.Mocked; }; +/** + * Creates a mocked input contract for the `EndpointAppContextService#setup()` method + */ +export const createMockEndpointAppContextServiceSetupContract = + (): jest.Mocked => { + return { + securitySolutionRequestContextFactory: requestContextFactoryMock.create(), + }; + }; + /** * Creates a mocked input contract for the `EndpointAppContextService#start()` method */ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked => { - const factory = new AppClientFactory(); const config = createMockConfig(); + const factory = new AppClientFactory(); + factory.setup({ getSpaceId: () => 'mockSpace', config }); + const casesClientMock = createCasesClientMock(); const savedObjectsStart = savedObjectsServiceMock.createStartContract(); const agentService = createMockAgentService(); @@ -86,8 +100,6 @@ export const createMockEndpointAppContextServiceStartContract = agentPolicyService ); - factory.setup({ getSpaceId: () => 'mockSpace', config }); - return { agentService, agentPolicyService, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index 5ce7962000788..c6df8c9183917 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -25,6 +25,7 @@ import { parseExperimentalConfigValue } from '../../../../common/experimental_fe import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; @@ -130,6 +131,7 @@ describe('Action Log API', () => { const esClientMock = elasticsearchServiceMock.createScopedClusterClient(); const routerMock = httpServiceMock.createRouter(); endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); registerActionAuditLogRoutes(routerMock, { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts index ee3bc5e1f21e3..a483a33ea4c8d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts @@ -18,6 +18,7 @@ import { parseExperimentalConfigValue } from '../../../../common/experimental_fe import { SecuritySolutionRequestHandlerContext } from '../../../types'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createMockPackageService, createRouteHandlerContext, @@ -157,9 +158,12 @@ describe('Host Isolation', () => { keep_policies_up_to_date: false, }) ); + licenseEmitter = new Subject(); licenseService = new LicenseService(); licenseService.start(licenseEmitter); + + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, licenseService, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts index facd53643bc4f..2f8ba30936f25 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.test.ts @@ -21,6 +21,7 @@ import { parseExperimentalConfigValue } from '../../../../common/experimental_fe import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; @@ -67,6 +68,7 @@ describe('Endpoint Action Status', () => { const esClientMock = elasticsearchServiceMock.createScopedClusterClient(); const routerMock = httpServiceMock.createRouter(); endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); registerActionStatusRoutes(routerMock, { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 3e5050c05814a..7c2e5de928484 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -22,6 +22,7 @@ import { HostInfo, HostResultList, HostStatus } from '../../../../common/endpoin import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { registerEndpointRoutes } from './index'; import { + createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createMockPackageService, createRouteHandlerContext, @@ -134,6 +135,7 @@ describe('test endpoint route', () => { keep_policies_up_to_date: false, }) ); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); mockAgentService = startContract.agentService!; @@ -394,6 +396,7 @@ describe('test endpoint route', () => { keep_policies_up_to_date: false, }) ); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); mockAgentService = startContract.agentService!; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 90cda7ceb05d4..f25171c6734c8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -7,6 +7,7 @@ import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockEndpointAppContextServiceSetupContract, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; @@ -45,6 +46,7 @@ describe('test policy response handler', () => { mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); }); @@ -161,6 +163,7 @@ describe('test policy response handler', () => { page: 1, perPage: 1, }; + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...createMockEndpointAppContextServiceStartContract(), ...{ agentService: mockAgentService }, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index d59ecb674196c..b25b599517300 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -9,6 +9,7 @@ import { ElasticsearchClient, Logger } from 'kibana/server'; import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; import { ApiResponse } from '@elastic/elasticsearch'; import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common'; +import { ENDPOINT_ACTION_RESPONSES_INDEX } from '../../../common/endpoint/constants'; import { SecuritySolutionRequestHandlerContext } from '../../types'; import { ActivityLog, @@ -146,6 +147,41 @@ const getActivityLog = async ({ return sortedData; }; +const hasAckInResponse = (response: EndpointActionResponse): boolean => { + return typeof response.action_data.ack !== 'undefined'; +}; + +// return TRUE if for given action_id/agent_id +// there is no doc in .logs-endpoint.action.response-default +const hasNoEndpointResponse = ({ + action, + agentId, + indexedActionIds, +}: { + action: EndpointAction; + agentId: string; + indexedActionIds: string[]; +}): boolean => { + return action.agents.includes(agentId) && !indexedActionIds.includes(action.action_id); +}; + +// return TRUE if for given action_id/agent_id +// there is no doc in .fleet-actions-results +const hasNoFleetResponse = ({ + action, + agentId, + agentResponses, +}: { + action: EndpointAction; + agentId: string; + agentResponses: EndpointActionResponse[]; +}): boolean => { + return ( + action.agents.includes(agentId) && + !agentResponses.map((e) => e.action_id).includes(action.action_id) + ); +}; + export const getPendingActionCounts = async ( esClient: ElasticsearchClient, metadataService: EndpointMetadataService, @@ -179,21 +215,45 @@ export const getPendingActionCounts = async ( .catch(catchAndWrapError); // retrieve any responses to those action IDs from these agents - const responses = await fetchActionResponseIds( + const responses = await fetchActionResponses( esClient, metadataService, recentActions.map((a) => a.action_id), agentIDs ); - const pending: EndpointPendingActions[] = []; + // + + const pending: EndpointPendingActions[] = []; for (const agentId of agentIDs) { - const responseIDsFromAgent = responses[agentId]; + const agentResponses = responses[agentId]; + + // get response actionIds for responses with ACKs + const ackResponseActionIdList: string[] = agentResponses + .filter(hasAckInResponse) + .map((response) => response.action_id); + + // actions Ids that are indexed in new response index + const indexedActionIds = await hasEndpointResponseDoc({ + agentId, + actionIds: ackResponseActionIdList, + esClient, + }); + + const pendingActions: EndpointAction[] = recentActions.filter((action) => { + return ackResponseActionIdList.includes(action.action_id) // if has ack + ? hasNoEndpointResponse({ action, agentId, indexedActionIds }) // then find responses in new index + : hasNoFleetResponse({ + // else use the legacy way + action, + agentId, + agentResponses, + }); + }); pending.push({ agent_id: agentId, - pending_actions: recentActions - .filter((a) => a.agents.includes(agentId) && !responseIDsFromAgent.includes(a.action_id)) + pending_actions: pendingActions .map((a) => a.data.command) .reduce((acc, cur) => { if (cur in acc) { @@ -209,6 +269,43 @@ export const getPendingActionCounts = async ( return pending; }; +/** + * Returns a boolean for search result + * + * @param esClient + * @param actionIds + * @param agentIds + */ +const hasEndpointResponseDoc = async ({ + actionIds, + agentId, + esClient, +}: { + actionIds: string[]; + agentId: string; + esClient: ElasticsearchClient; +}): Promise => { + const response = await esClient + .search( + { + index: ENDPOINT_ACTION_RESPONSES_INDEX, + body: { + query: { + bool: { + filter: [{ terms: { action_id: actionIds } }, { term: { agent_id: agentId } }], + }, + }, + }, + }, + { ignore: [404] } + ) + .then( + (result) => result.body?.hits?.hits?.map((a) => a._source?.EndpointActions.action_id) || [] + ) + .catch(catchAndWrapError); + return response.filter((action): action is string => action !== undefined); +}; + /** * Returns back a map of elastic Agent IDs to array of Action IDs that have received a response. * @@ -217,16 +314,19 @@ export const getPendingActionCounts = async ( * @param actionIds * @param agentIds */ -const fetchActionResponseIds = async ( +const fetchActionResponses = async ( esClient: ElasticsearchClient, metadataService: EndpointMetadataService, actionIds: string[], agentIds: string[] -): Promise> => { - const actionResponsesByAgentId: Record = agentIds.reduce((acc, agentId) => { - acc[agentId] = []; - return acc; - }, {} as Record); +): Promise> => { + const actionResponsesByAgentId: Record = agentIds.reduce( + (acc, agentId) => { + acc[agentId] = []; + return acc; + }, + {} as Record + ); const actionResponses = await esClient .search( @@ -255,7 +355,7 @@ const fetchActionResponseIds = async ( return actionResponsesByAgentId; } - // Get the latest docs from the metadata datastream for the Elastic Agent IDs in the action responses + // Get the latest docs from the metadata data-stream for the Elastic Agent IDs in the action responses // This will be used determine if we should withhold the action id from the returned list in cases where // the Endpoint might not yet have sent an updated metadata document (which would be representative of // the state of the endpoint post-action) @@ -288,7 +388,7 @@ const fetchActionResponseIds = async ( enoughTimeHasLapsed || lastEndpointMetadataEventTimestamp > actionCompletedAtTimestamp ) { - actionResponsesByAgentId[actionResponse.agent_id].push(actionResponse.action_id); + actionResponsesByAgentId[actionResponse.agent_id].push(actionResponse); } } diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 2f31f54143f74..71c093e0781b0 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; + import { httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; import { createNewPackagePolicyMock, deletePackagePolicyMock } from '../../../fleet/common/mocks'; import { @@ -18,7 +20,8 @@ import { getPackagePolicyUpdateCallback, } from './fleet_integration'; import { KibanaRequest } from 'kibana/server'; -import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__'; +import { requestContextMock } from '../lib/detection_engine/routes/__mocks__'; +import { requestContextFactoryMock } from '../request_context_factory.mock'; import { EndpointAppContextServiceStartContract } from '../endpoint/endpoint_app_context_services'; import { createMockEndpointAppContextServiceStartContract } from '../endpoint/mocks'; import { licenseMock } from '../../../licensing/common/licensing.mock'; @@ -42,16 +45,12 @@ import { ExperimentalFeatures, } from '../../common/experimental_features'; import { DeletePackagePoliciesResponse } from '../../../fleet/common'; -import { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; describe('ingest_integration tests ', () => { let endpointAppContextMock: EndpointAppContextServiceStartContract; let req: KibanaRequest; let ctx: SecuritySolutionRequestHandlerContext; const exceptionListClient: ExceptionListClient = getExceptionListClientMock(); - const maxTimelineImportExportSize = createMockConfig().maxTimelineImportExportSize; - const prebuiltRulesFromFileSystem = createMockConfig().prebuiltRulesFromFileSystem; - const prebuiltRulesFromSavedObjects = createMockConfig().prebuiltRulesFromSavedObjects; let licenseEmitter: Subject; let licenseService: LicenseService; const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -88,11 +87,7 @@ describe('ingest_integration tests ', () => { const callback = getPackagePolicyCreateCallback( logger, manifestManager, - endpointAppContextMock.appClientFactory, - maxTimelineImportExportSize, - prebuiltRulesFromFileSystem, - prebuiltRulesFromSavedObjects, - endpointAppContextMock.security, + requestContextFactoryMock.create(), endpointAppContextMock.alerting, licenseService, exceptionListClient diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index 09810a6c88c3d..a53d5d43de524 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -8,7 +8,6 @@ import { KibanaRequest, Logger, RequestHandlerContext } from 'kibana/server'; import { ExceptionListClient } from '../../../lists/server'; import { PluginStartContract as AlertsStartContract } from '../../../alerting/server'; -import { SecurityPluginStart } from '../../../security/server'; import { PostPackagePolicyCreateCallback, PostPackagePolicyDeleteCallback, @@ -18,15 +17,15 @@ import { import { NewPackagePolicy, UpdatePackagePolicy } from '../../../fleet/common'; import { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; -import { ManifestManager } from '../endpoint/services'; -import { AppClientFactory } from '../client'; +import { ExperimentalFeatures } from '../../common/experimental_features'; import { LicenseService } from '../../common/license'; +import { ManifestManager } from '../endpoint/services'; +import { IRequestContextFactory } from '../request_context_factory'; import { installPrepackagedRules } from './handlers/install_prepackaged_rules'; import { createPolicyArtifactManifest } from './handlers/create_policy_artifact_manifest'; import { createDefaultPolicy } from './handlers/create_default_policy'; import { validatePolicyAgainstLicense } from './handlers/validate_policy_against_license'; import { removePolicyFromTrustedApps } from './handlers/remove_policy_from_trusted_apps'; -import { ExperimentalFeatures } from '../../common/experimental_features'; const isEndpointPackagePolicy = ( packagePolicy: T @@ -40,11 +39,7 @@ const isEndpointPackagePolicy = ( export const getPackagePolicyCreateCallback = ( logger: Logger, manifestManager: ManifestManager, - appClientFactory: AppClientFactory, - maxTimelineImportExportSize: number, - prebuiltRulesFromFileSystem: boolean, - prebuiltRulesFromSavedObjects: boolean, - securityStart: SecurityPluginStart, + securitySolutionRequestContextFactory: IRequestContextFactory, alerts: AlertsStartContract, licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined @@ -59,20 +54,23 @@ export const getPackagePolicyCreateCallback = ( return newPackagePolicy; } + // In this callback we are handling an HTTP request to the fleet plugin. Since we use + // code from the security_solution plugin to handle it (installPrepackagedRules), + // we need to build the context that is native to security_solution and pass it there. + const securitySolutionContext = await securitySolutionRequestContextFactory.create( + context, + request + ); + // perform these operations in parallel in order to help in not delaying the API response too much const [, manifestValue] = await Promise.all([ // Install Detection Engine prepackaged rules exceptionsClient && installPrepackagedRules({ logger, - appClientFactory, - context, + context: securitySolutionContext, request, - securityStart, alerts, - maxTimelineImportExportSize, - prebuiltRulesFromFileSystem, - prebuiltRulesFromSavedObjects, exceptionsClient, }), diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts index d8adf4ea6a1ca..01368ccb22c62 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts @@ -5,25 +5,18 @@ * 2.0. */ -import { KibanaRequest, Logger, RequestHandlerContext } from 'kibana/server'; +import { KibanaRequest, Logger } from 'kibana/server'; import { ExceptionListClient } from '../../../../lists/server'; import { PluginStartContract as AlertsStartContract } from '../../../../alerting/server'; -import { SecurityPluginStart } from '../../../../security/server'; -import { AppClientFactory } from '../../client'; import { createDetectionIndex } from '../../lib/detection_engine/routes/index/create_index_route'; import { createPrepackagedRules } from '../../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; -import { buildFrameworkRequest } from '../../lib/timeline/utils/common'; +import { SecuritySolutionApiRequestHandlerContext } from '../../types'; export interface InstallPrepackagedRulesProps { logger: Logger; - appClientFactory: AppClientFactory; - context: RequestHandlerContext; + context: SecuritySolutionApiRequestHandlerContext; request: KibanaRequest; - securityStart: SecurityPluginStart; alerts: AlertsStartContract; - maxTimelineImportExportSize: number; - prebuiltRulesFromFileSystem: boolean; - prebuiltRulesFromSavedObjects: boolean; exceptionsClient: ExceptionListClient; } @@ -33,29 +26,14 @@ export interface InstallPrepackagedRulesProps { */ export const installPrepackagedRules = async ({ logger, - appClientFactory, context, request, - securityStart, alerts, - maxTimelineImportExportSize, - prebuiltRulesFromFileSystem, - prebuiltRulesFromSavedObjects, exceptionsClient, }: InstallPrepackagedRulesProps): Promise => { - // prep for detection rules creation - const appClient = appClientFactory.create(request); - - // This callback is called by fleet plugin. - // It doesn't have access to SecuritySolutionRequestHandlerContext in runtime. - // Muting the error to have green CI. - // @ts-expect-error - const frameworkRequest = await buildFrameworkRequest(context, securityStart, request); - // Create detection index & rules (if necessary). move past any failure, this is just a convenience try { - // @ts-expect-error - await createDetectionIndex(context, appClient); + await createDetectionIndex(context); } catch (err) { if (err.statusCode !== 409) { // 409 -> detection index already exists, which is fine @@ -68,14 +46,8 @@ export const installPrepackagedRules = async ({ // this checks to make sure index exists first, safe to try in case of failure above // may be able to recover from minor errors await createPrepackagedRules( - // @ts-expect-error context, - appClient, alerts.getRulesClientWithRequest(request), - frameworkRequest, - maxTimelineImportExportSize, - prebuiltRulesFromFileSystem, - prebuiltRulesFromSavedObjects, exceptionsClient ); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index 7e3da726f6ebe..0adcd25f5e246 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { Plugin, PluginSetup, PluginStart } from './plugin'; -import { configSchema, ConfigType } from './config'; +import { configSchema, ConfigSchema, ConfigType } from './config'; import { SIGNALS_INDEX_KEY } from '../common/constants'; import { AppClient } from './types'; @@ -15,7 +15,7 @@ export const plugin = (context: PluginInitializerContext) => { return new Plugin(context); }; -export const config: PluginConfigDescriptor = { +export const config: PluginConfigDescriptor = { exposeToBrowser: { enableExperimental: true, }, @@ -47,5 +47,5 @@ export const config: PluginConfigDescriptor = { export { ConfigType, Plugin, PluginSetup, PluginStart }; export { AppClient }; -export type { AppRequestContext } from './types'; +export type { SecuritySolutionApiRequestHandlerContext } from './types'; export { EndpointError } from './endpoint/errors'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts index 8417115fb1896..cf0ceaff4ec4c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts @@ -5,34 +5,11 @@ * 2.0. */ -import { DEFAULT_SIGNALS_INDEX, SIGNALS_INDEX_KEY } from '../../../../../common/constants'; -import { requestContextMock } from './request_context'; -import { serverMock } from './server'; -import { requestMock } from './request'; -import { responseMock } from './response_factory'; -import { ConfigType } from '../../../../config'; -import { UnderlyingLogClient } from '../../rule_execution_log/types'; - -export { requestMock, requestContextMock, responseMock, serverMock }; - -export const createMockConfig = (): ConfigType => ({ - [SIGNALS_INDEX_KEY]: DEFAULT_SIGNALS_INDEX, - maxRuleImportExportSize: 10000, - maxRuleImportPayloadBytes: 10485760, - maxTimelineImportExportSize: 10000, - maxTimelineImportPayloadBytes: 10485760, - enableExperimental: [], - endpointResultListDefaultFirstPageIndex: 0, - endpointResultListDefaultPageSize: 10, - packagerTaskInterval: '60s', - alertMergeStrategy: 'missingFields', - alertIgnoreFields: [], - prebuiltRulesFromFileSystem: true, - prebuiltRulesFromSavedObjects: false, - ruleExecutionLog: { - underlyingClient: UnderlyingLogClient.savedObjects, - }, -}); +export { requestContextMock } from './request_context'; +export { requestMock } from './request'; +export { responseMock } from './response_factory'; +export { serverMock } from './server'; +export { configMock, createMockConfig } from '../../../../config.mock'; export const mockGetCurrentUser = { user: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 6039ad6ab6126..2f5f8ac846954 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -5,66 +5,103 @@ * 2.0. */ -import type { SecuritySolutionRequestHandlerContext } from '../../../../types'; -import { - coreMock, - elasticsearchServiceMock, - savedObjectsClientMock, -} from '../../../../../../../../src/core/server/mocks'; +import type { MockedKeys } from '@kbn/utility-types/jest'; +import { coreMock } from 'src/core/server/mocks'; + +import { ActionsApiRequestHandlerContext } from '../../../../../../actions/server'; +import { AlertingApiRequestHandlerContext } from '../../../../../../alerting/server'; import { rulesClientMock } from '../../../../../../alerting/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; +import { listMock } from '../../../../../../lists/server/mocks'; +import { ruleRegistryMocks } from '../../../../../../rule_registry/server/mocks'; + import { siemMock } from '../../../../mocks'; +import { createMockConfig } from '../../../../config.mock'; import { ruleExecutionLogClientMock } from '../../rule_execution_log/__mocks__/rule_execution_log_client'; +import { requestMock } from './request'; +import { internalFrameworkRequest } from '../../../framework'; -const createMockClients = () => ({ - rulesClient: rulesClientMock.create(), - licensing: { license: licensingMock.createLicenseMock() }, - clusterClient: elasticsearchServiceMock.createScopedClusterClient(), - savedObjectsClient: savedObjectsClientMock.create(), - ruleExecutionLogClient: ruleExecutionLogClientMock.create(), - appClient: siemMock.createClient(), -}); - -/** - * Adds mocking to the interface so we don't have to cast everywhere - */ -type SecuritySolutionRequestHandlerContextMock = SecuritySolutionRequestHandlerContext & { - core: { - elasticsearch: { - client: { - asCurrentUser: { - updateByQuery: jest.Mock; - search: jest.Mock; - security: { - hasPrivileges: jest.Mock; - }; - }; - }; - }; +import type { + SecuritySolutionApiRequestHandlerContext, + SecuritySolutionRequestHandlerContext, +} from '../../../../types'; + +const createMockClients = () => { + const core = coreMock.createRequestHandlerContext(); + const license = licensingMock.createLicenseMock(); + + return { + core, + clusterClient: core.elasticsearch.client, + savedObjectsClient: core.savedObjects.client, + + licensing: { + ...licensingMock.createRequestHandlerContext({ license }), + license, + }, + lists: { + listClient: listMock.getListClient(), + exceptionListClient: listMock.getExceptionListClient(core.savedObjects.client), + }, + rulesClient: rulesClientMock.create(), + ruleDataService: ruleRegistryMocks.createRuleDataService(), + + config: createMockConfig(), + appClient: siemMock.createClient(), + ruleExecutionLogClient: ruleExecutionLogClientMock.create(), }; }; +type MockClients = ReturnType; + +type SecuritySolutionRequestHandlerContextMock = + MockedKeys & { + core: MockClients['core']; + }; + const createRequestContextMock = ( - clients: ReturnType = createMockClients() + clients: MockClients = createMockClients() ): SecuritySolutionRequestHandlerContextMock => { - const coreContext = coreMock.createRequestHandlerContext(); return { - alerting: { getRulesClient: jest.fn(() => clients.rulesClient) }, - core: { - ...coreContext, - elasticsearch: { - ...coreContext.elasticsearch, - client: clients.clusterClient, - }, - savedObjects: { client: clients.savedObjectsClient }, - }, + core: clients.core, + securitySolution: createSecuritySolutionRequestContextMock(clients), + actions: {} as unknown as jest.Mocked, + alerting: { + getRulesClient: jest.fn(() => clients.rulesClient), + } as unknown as jest.Mocked, licensing: clients.licensing, - securitySolution: { - getAppClient: jest.fn(() => clients.appClient), - getExecutionLogClient: jest.fn(() => clients.ruleExecutionLogClient), - getSpaceId: jest.fn(() => 'default'), + lists: { + getListClient: jest.fn(() => clients.lists.listClient), + getExceptionListClient: jest.fn(() => clients.lists.exceptionListClient), }, - } as unknown as SecuritySolutionRequestHandlerContextMock; + }; +}; + +const createSecuritySolutionRequestContextMock = ( + clients: MockClients +): jest.Mocked => { + const core = clients.core; + const kibanaRequest = requestMock.create(); + + return { + core, + getConfig: jest.fn(() => clients.config), + getFrameworkRequest: jest.fn(() => { + return { + ...kibanaRequest.body, + [internalFrameworkRequest]: kibanaRequest, + context: { core }, + user: { + username: 'mockUser', + }, + }; + }), + getAppClient: jest.fn(() => clients.appClient), + getSpaceId: jest.fn(() => 'default'), + getRuleDataService: jest.fn(() => clients.ruleDataService), + getExecutionLogClient: jest.fn(() => clients.ruleExecutionLogClient), + getExceptionListClient: jest.fn(() => clients.lists.exceptionListClient), + }; }; const createTools = () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 9d1cd3cbca3fb..1520b4da82d8d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { SavedObjectsFindResponse, SavedObjectsFindResult } from 'kibana/server'; +import type { estypes } from '@elastic/elasticsearch'; +import { SavedObjectsFindResponse, SavedObjectsFindResult } from 'src/core/server'; + import { ActionResult } from '../../../../../../actions/server'; import { SignalSearchResponse } from '../../signals/types'; import { @@ -562,6 +564,28 @@ export const getFindBulkResultStatus = (): FindBulkExecutionLogResponse => ({ ], }); +export const getBasicEmptySearchResponse = (): estypes.SearchResponse => ({ + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits: [], + total: { relation: 'eq', value: 0 }, + max_score: 0, + }, +}); + +export const getBasicNoShardsSearchResponse = (): estypes.SearchResponse => ({ + took: 1, + timed_out: false, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + hits: { + hits: [], + total: { relation: 'eq', value: 0 }, + max_score: 0, + }, +}); + export const getEmptySignalsResponse = (): SignalSearchResponse => ({ took: 1, timed_out: false, @@ -588,7 +612,7 @@ export const getEmptyEqlSequencesResponse = (): EqlSearchResponse => ({ timed_out: false, }); -export const getSuccessfulSignalUpdateResponse = () => ({ +export const getSuccessfulSignalUpdateResponse = (): estypes.UpdateByQueryResponse => ({ took: 18, timed_out: false, total: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index 6ec23c32e4976..b011fd3fcd247 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -16,9 +16,8 @@ import { createBootstrapIndex, } from '@kbn/securitysolution-es-utils'; import type { - AppClient, + SecuritySolutionApiRequestHandlerContext, SecuritySolutionPluginRouter, - SecuritySolutionRequestHandlerContext, } from '../../../../types'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; @@ -32,15 +31,8 @@ import signalsPolicy from './signals_policy.json'; import { templateNeedsUpdate } from './check_template_version'; import { getIndexVersion } from './get_index_version'; import { isOutdated } from '../../migrations/helpers'; -import { RuleDataPluginService } from '../../../../../../rule_registry/server'; -import { ConfigType } from '../../../../config'; -import { parseExperimentalConfigValue } from '../../../../../common/experimental_features'; -export const createIndexRoute = ( - router: SecuritySolutionPluginRouter, - ruleDataService: RuleDataPluginService, - config: ConfigType -) => { +export const createIndexRoute = (router: SecuritySolutionPluginRouter) => { router.post( { path: DETECTION_ENGINE_INDEX_URL, @@ -51,14 +43,13 @@ export const createIndexRoute = ( }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - const { ruleRegistryEnabled } = parseExperimentalConfigValue(config.enableExperimental); try { const siemClient = context.securitySolution?.getAppClient(); if (!siemClient) { return siemResponse.error({ statusCode: 404 }); } - await createDetectionIndex(context, siemClient, ruleDataService, ruleRegistryEnabled); + await createDetectionIndex(context.securitySolution); return response.ok({ body: { acknowledged: true } }); } catch (err) { const error = transformError(err); @@ -71,30 +62,18 @@ export const createIndexRoute = ( ); }; -class CreateIndexError extends Error { - public readonly statusCode: number; - constructor(message: string, statusCode: number) { - super(message); - this.statusCode = statusCode; - } -} - export const createDetectionIndex = async ( - context: SecuritySolutionRequestHandlerContext, - siemClient: AppClient, - ruleDataService: RuleDataPluginService, - ruleRegistryEnabled: boolean + context: SecuritySolutionApiRequestHandlerContext ): Promise => { + const config = context.getConfig(); const esClient = context.core.elasticsearch.client.asCurrentUser; - const spaceId = siemClient.getSpaceId(); - - if (!siemClient) { - throw new CreateIndexError('', 404); - } - + const siemClient = context.getAppClient(); + const spaceId = context.getSpaceId(); const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(esClient, index); + const { ruleRegistryEnabled } = config.experimentalFeatures; + // If using the rule registry implementation, we don't want to create new .siem-signals indices - // only create/update resources if there are existing indices if (ruleRegistryEnabled && !indexExists) { @@ -106,7 +85,10 @@ export const createDetectionIndex = async ( if (!policyExists) { await setPolicy(esClient, index, signalsPolicy); } + + const ruleDataService = context.getRuleDataService(); const aadIndexAliasName = ruleDataService.getResourceName(`security.alerts-${spaceId}`); + if (await templateNeedsUpdate({ alias: index, esClient })) { await esClient.indices.putIndexTemplate({ name: index, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 7ffa45e2bf7ee..2c2c65f5f78f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -19,9 +19,9 @@ describe('read_privileges route', () => { server = serverMock.create(); ({ context } = requestContextMock.createTools()); - context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockResolvedValue({ - body: getMockPrivilegesResult(), - }); + context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(getMockPrivilegesResult()) + ); readPrivilegesRoute(server.router, true); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 26e09d69d3a45..29ceb74e9ba0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -10,14 +10,13 @@ import { addPrepackagedRulesRequest, getFindResultWithSingleHit, getAlertMock, + getBasicEmptySearchResponse, + getBasicNoShardsSearchResponse, } from '../__mocks__/request_responses'; -import { requestContextMock, serverMock, createMockConfig, mockGetCurrentUser } from '../__mocks__'; +import { configMock, requestContextMock, serverMock } from '../__mocks__'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; -import { SecurityPluginSetup } from '../../../../../../security/server'; import { addPrepackedRulesRoute, createPrepackagedRules } from './add_prepackaged_rules_route'; import { listMock } from '../../../../../../lists/server/mocks'; -import { siemMock } from '../../../../mocks'; -import { FrameworkRequest } from '../../../framework'; import { ExceptionListClient } from '../../../../../../lists/server'; import { installPrepackagedTimelines } from '../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -76,25 +75,21 @@ describe.each([ ['Legacy', false], ['RAC', true], ])('add_prepackaged_rules_route - %s', (_, isRuleRegistryEnabled) => { - const siemMockClient = siemMock.createClient(); let server: ReturnType; let { clients, context } = requestContextMock.createTools(); - let securitySetup: SecurityPluginSetup; let mockExceptionsClient: ExceptionListClient; const testif = isRuleRegistryEnabled ? test.skip : test; + const defaultConfig = context.securitySolution.getConfig(); beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); - securitySetup = { - authc: { - getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), - }, - authz: {}, - } as unknown as SecurityPluginSetup; - mockExceptionsClient = listMock.getExceptionListClient(); + context.securitySolution.getConfig.mockImplementation(() => + configMock.withRuleRegistryEnabled(defaultConfig, isRuleRegistryEnabled) + ); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); clients.rulesClient.update.mockResolvedValue( getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) @@ -110,9 +105,9 @@ describe.each([ }); context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse()) ); - addPrepackedRulesRoute(server.router, createMockConfig(), securitySetup, isRuleRegistryEnabled); + addPrepackedRulesRoute(server.router); }); describe('status codes', () => { @@ -138,7 +133,9 @@ describe.each([ test('it returns a 400 if the index does not exist when rule registry not enabled', async () => { const request = addPrepackagedRulesRequest(); context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + getBasicNoShardsSearchResponse() + ) ); const response = await server.inject(request, context); @@ -302,39 +299,22 @@ describe.each([ describe('createPrepackagedRules', () => { test('uses exception lists client from context when available', async () => { - context.lists = { - getExceptionListClient: jest.fn(), - getListClient: jest.fn(), - }; - const config = createMockConfig(); - await createPrepackagedRules( - context, - siemMockClient, + context.securitySolution, clients.rulesClient, - {} as FrameworkRequest, - 1200, - config.prebuiltRulesFromFileSystem, - config.prebuiltRulesFromSavedObjects, mockExceptionsClient ); expect(mockExceptionsClient.createEndpointList).not.toHaveBeenCalled(); - expect(context.lists?.getExceptionListClient).toHaveBeenCalled(); + expect(context.securitySolution.getExceptionListClient).toHaveBeenCalled(); }); test('uses passed in exceptions list client when lists client not available in context', async () => { - const { lists, ...myContext } = context; - const config = createMockConfig(); + context.securitySolution.getExceptionListClient.mockImplementation(() => null); await createPrepackagedRules( - myContext, - siemMockClient, + context.securitySolution, clients.rulesClient, - {} as FrameworkRequest, - 1200, - config.prebuiltRulesFromFileSystem, - config.prebuiltRulesFromSavedObjects, mockExceptionsClient ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index ddf4e956beac4..50766af669ce7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -9,9 +9,8 @@ import moment from 'moment'; import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; import { validate } from '@kbn/securitysolution-io-ts-utils'; import type { - AppClient, + SecuritySolutionApiRequestHandlerContext, SecuritySolutionPluginRouter, - SecuritySolutionRequestHandlerContext, } from '../../../../types'; import { @@ -21,10 +20,6 @@ import { import { importTimelineResultSchema } from '../../../../../common/types/timeline'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { ConfigType } from '../../../../config'; -import { SetupPlugins } from '../../../../plugin'; -import { buildFrameworkRequest } from '../../../timeline/utils/common'; - import { getLatestPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { installPrepackagedRules } from '../../rules/install_prepacked_rules'; import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; @@ -35,17 +30,11 @@ import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset/rule_ import { buildSiemResponse } from '../utils'; import { RulesClient } from '../../../../../../alerting/server'; -import { FrameworkRequest } from '../../../framework'; import { ExceptionListClient } from '../../../../../../lists/server'; import { installPrepackagedTimelines } from '../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; -export const addPrepackedRulesRoute = ( - router: SecuritySolutionPluginRouter, - config: ConfigType, - security: SetupPlugins['security'], - isRuleRegistryEnabled: boolean -) => { +export const addPrepackedRulesRoute = (router: SecuritySolutionPluginRouter) => { router.put( { path: DETECTION_ENGINE_PREPACKAGED_URL, @@ -63,7 +52,6 @@ export const addPrepackedRulesRoute = ( }, async (context, _, response) => { const siemResponse = buildSiemResponse(response); - const frameworkRequest = await buildFrameworkRequest(context, security, _); try { const rulesClient = context.alerting?.getRulesClient(); @@ -74,15 +62,9 @@ export const addPrepackedRulesRoute = ( } const validated = await createPrepackagedRules( - context, - siemClient, + context.securitySolution, rulesClient, - frameworkRequest, - config.maxTimelineImportExportSize, - config.prebuiltRulesFromFileSystem, - config.prebuiltRulesFromSavedObjects, - undefined, - isRuleRegistryEnabled + undefined ); return response.ok({ body: validated ?? {} }); } catch (err) { @@ -105,22 +87,26 @@ class PrepackagedRulesError extends Error { } export const createPrepackagedRules = async ( - context: SecuritySolutionRequestHandlerContext, - siemClient: AppClient, + context: SecuritySolutionApiRequestHandlerContext, rulesClient: RulesClient, - frameworkRequest: FrameworkRequest, - maxTimelineImportExportSize: ConfigType['maxTimelineImportExportSize'], - prebuiltRulesFromFileSystem: ConfigType['prebuiltRulesFromFileSystem'], - prebuiltRulesFromSavedObjects: ConfigType['prebuiltRulesFromSavedObjects'], - exceptionsClient?: ExceptionListClient, - isRuleRegistryEnabled?: boolean | undefined + exceptionsClient?: ExceptionListClient ): Promise => { + const config = context.getConfig(); + const frameworkRequest = context.getFrameworkRequest(); const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; - const exceptionsListClient = - context.lists != null ? context.lists.getExceptionListClient() : exceptionsClient; + const siemClient = context.getAppClient(); + const exceptionsListClient = context.getExceptionListClient() ?? exceptionsClient; const ruleAssetsClient = ruleAssetSavedObjectsClientFactory(savedObjectsClient); - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + const ruleStatusClient = context.getExecutionLogClient(); + + const { + maxTimelineImportExportSize, + prebuiltRulesFromFileSystem, + prebuiltRulesFromSavedObjects, + experimentalFeatures: { ruleRegistryEnabled }, + } = config; + if (!siemClient || !rulesClient) { throw new PrepackagedRulesError('', 404); } @@ -137,12 +123,12 @@ export const createPrepackagedRules = async ( ); const prepackagedRules = await getExistingPrepackagedRules({ rulesClient, - isRuleRegistryEnabled: isRuleRegistryEnabled ?? false, + isRuleRegistryEnabled: ruleRegistryEnabled, }); const rulesToInstall = getRulesToInstall(latestPrepackagedRules, prepackagedRules); const rulesToUpdate = getRulesToUpdate(latestPrepackagedRules, prepackagedRules); const signalsIndex = siemClient.getSignalsIndex(); - if (!isRuleRegistryEnabled && (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0)) { + if (!ruleRegistryEnabled && (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0)) { const signalsIndexExists = await getIndexExists(esClient.asCurrentUser, signalsIndex); if (!signalsIndexExists) { throw new PrepackagedRulesError( @@ -153,12 +139,7 @@ export const createPrepackagedRules = async ( } await Promise.all( - installPrepackagedRules( - rulesClient, - rulesToInstall, - signalsIndex, - isRuleRegistryEnabled ?? false - ) + installPrepackagedRules(rulesClient, rulesToInstall, signalsIndex, ruleRegistryEnabled) ); const timeline = await installPrepackagedTimelines( maxTimelineImportExportSize, @@ -172,11 +153,11 @@ export const createPrepackagedRules = async ( await updatePrepackagedRules( rulesClient, savedObjectsClient, - context.securitySolution.getSpaceId(), + context.getSpaceId(), ruleStatusClient, rulesToUpdate, signalsIndex, - isRuleRegistryEnabled ?? false + ruleRegistryEnabled ); const prepackagedRulesOutput: PrePackagedRulesAndTimelinesSchema = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 6f721bb2bb9c5..6dc303d5a266b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -14,6 +14,8 @@ import { getEmptyFindResult, getAlertMock, createBulkMlRuleRequest, + getBasicEmptySearchResponse, + getBasicNoShardsSearchResponse, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; @@ -43,7 +45,7 @@ describe.each([ ); // successful creation context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse()) ); createRulesBulkRoute(server.router, ml, isRuleRegistryEnabled); }); @@ -93,7 +95,9 @@ describe.each([ test('returns an error object if the index does not exist when rule registry not enabled', async () => { context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + getBasicNoShardsSearchResponse() + ) ); const response = await server.inject(getReadBulkRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 59fe5c0ff68a1..010c4b27507bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -13,6 +13,8 @@ import { getRuleExecutionStatuses, getFindResultWithSingleHit, createMlRuleRequest, + getBasicEmptySearchResponse, + getBasicNoShardsSearchResponse, } from '../__mocks__/request_responses'; import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; import { buildMlAuthz } from '../../../machine_learning/authz'; @@ -44,7 +46,7 @@ describe.each([ clients.ruleExecutionLogClient.find.mockResolvedValue(getRuleExecutionStatuses()); // needed to transform: ; context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse()) ); createRulesRoute(server.router, ml, isRuleRegistryEnabled); }); @@ -103,7 +105,9 @@ describe.each([ describe('unhappy paths', () => { test('it returns a 400 if the index does not exist when rule registry not enabled', async () => { context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + getBasicNoShardsSearchResponse() + ) ); const response = await server.inject(getCreateRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts index c84dd8147ebcc..277590820850b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -46,6 +46,7 @@ export const exportRulesRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); const rulesClient = context.alerting?.getRulesClient(); + const exceptionsClient = context.lists?.getExceptionListClient(); const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { @@ -72,20 +73,27 @@ export const exportRulesRoute = ( } } - const exported = + const exportedRulesAndExceptions = request.body?.objects != null ? await getExportByObjectIds( rulesClient, + exceptionsClient, savedObjectsClient, request.body.objects, logger, isRuleRegistryEnabled ) - : await getExportAll(rulesClient, savedObjectsClient, logger, isRuleRegistryEnabled); + : await getExportAll( + rulesClient, + exceptionsClient, + savedObjectsClient, + logger, + isRuleRegistryEnabled + ); const responseBody = request.query.exclude_export_details - ? exported.rulesNdjson - : `${exported.rulesNdjson}${exported.exportDetails}`; + ? exportedRulesAndExceptions.rulesNdjson + : `${exportedRulesAndExceptions.rulesNdjson}${exportedRulesAndExceptions.exceptionLists}${exportedRulesAndExceptions.exportDetails}`; return response.ok({ headers: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index aa301bcc0335e..23779afdc5410 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -12,6 +12,8 @@ import { getEmptyFindResult, getAlertMock, getFindResultWithSingleHit, + getBasicEmptySearchResponse, + getBasicNoShardsSearchResponse, } from '../__mocks__/request_responses'; import { createMockConfig, requestContextMock, serverMock, requestMock } from '../__mocks__'; import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; @@ -52,7 +54,7 @@ describe.each([ getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse()) ); importRulesRoute(server.router, config, ml, isRuleRegistryEnabled); }); @@ -133,7 +135,9 @@ describe.each([ test('returns an error if the index does not exist when rule registry not enabled', async () => { clients.appClient.getSignalsIndex.mockReturnValue('mockSignalsIndex'); context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + getBasicNoShardsSearchResponse() + ) ); const response = await server.inject(request, context); expect(response.status).toEqual(isRuleRegistryEnabled ? 200 : 400); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index fb5a2315479da..d043149f8474e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -47,6 +47,7 @@ export const performBulkActionRoute = ( try { const rulesClient = context.alerting?.getRulesClient(); + const exceptionsClient = context.lists?.getExceptionListClient(); const savedObjectsClient = context.core.savedObjects.client; const ruleStatusClient = context.securitySolution.getExecutionLogClient(); @@ -136,13 +137,14 @@ export const performBulkActionRoute = ( case BulkAction.export: const exported = await getExportByObjectIds( rulesClient, + exceptionsClient, savedObjectsClient, rules.data.map(({ params }) => ({ rule_id: params.ruleId })), logger, isRuleRegistryEnabled ); - const responseBody = `${exported.rulesNdjson}${exported.exportDetails}`; + const responseBody = `${exported.rulesNdjson}${exported.exceptionLists}${exported.exportDetails}`; return response.ok({ headers: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index f6abfc9ebe3d1..07c3bc37e7d72 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -26,11 +26,13 @@ describe('set signal status', () => { beforeEach(() => { server = serverMock.create(); ({ context } = requestContextMock.createTools()); - context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( + + context.core.elasticsearch.client.asCurrentUser.updateByQuery.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise( getSuccessfulSignalUpdateResponse() ) ); + setSignalsStatusRoute(server.router); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 0fe7cbdc9bd9f..c2e4b926d6375 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -6,13 +6,8 @@ */ import { isEmpty } from 'lodash'; -import { flow } from 'fp-ts/lib/function'; -import { Either, chain, fold, tryCatch } from 'fp-ts/lib/Either'; - -import { TIMESTAMP } from '@kbn/rule-data-utils'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; -import { toError } from '@kbn/securitysolution-list-api'; import { createPersistenceRuleTypeWrapper } from '../../../../../rule_registry/server'; import { buildRuleMessageFactory } from './factories/build_rule_message_factory'; @@ -136,58 +131,46 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const inputIndices = params.index ?? []; - const [privileges, timestampFieldCaps] = await Promise.all([ - checkPrivilegesFromEsClient(esClient, inputIndices), - esClient.fieldCaps({ - index: index ?? ['*'], - fields: hasTimestampOverride - ? [TIMESTAMP, timestampOverride as string] - : [TIMESTAMP], - include_unmapped: true, - }), - ]); - - fold, void>( - async (error: Error) => logger.error(buildRuleMessage(error.message)), - async (status: Promise) => (wroteWarningStatus = await status) - )( - flow( - () => - tryCatch( - () => - hasReadIndexPrivileges({ - ...basicLogArguments, - privileges, - logger, - buildRuleMessage, - ruleStatusClient, - }), - toError - ), - chain((wroteStatus: unknown) => - tryCatch( - () => - hasTimestampFields({ - ...basicLogArguments, - wroteStatus: wroteStatus as boolean, - timestampField: hasTimestampOverride - ? (timestampOverride as string) - : '@timestamp', - ruleName: name, - timestampFieldCapsResponse: timestampFieldCaps, - inputIndices, - ruleStatusClient, - logger, - buildRuleMessage, - }), - toError - ) - ) - )() as Either> - ); + const privileges = await checkPrivilegesFromEsClient(esClient, inputIndices); + + wroteWarningStatus = await hasReadIndexPrivileges({ + ...basicLogArguments, + privileges, + logger, + buildRuleMessage, + ruleStatusClient, + }); + + if (!wroteWarningStatus) { + const timestampFieldCaps = await services.scopedClusterClient.asCurrentUser.fieldCaps( + { + index, + fields: hasTimestampOverride + ? ['@timestamp', timestampOverride as string] + : ['@timestamp'], + include_unmapped: true, + } + ); + wroteWarningStatus = await hasTimestampFields({ + ...basicLogArguments, + timestampField: hasTimestampOverride ? (timestampOverride as string) : '@timestamp', + timestampFieldCapsResponse: timestampFieldCaps, + inputIndices, + ruleStatusClient, + logger, + buildRuleMessage, + }); + } } } catch (exc) { - logger.error(buildRuleMessage(`Check privileges failed to execute ${exc}`)); + const errorMessage = buildRuleMessage(`Check privileges failed to execute ${exc}`); + logger.error(errorMessage); + await ruleStatusClient.logStatusChange({ + ...basicLogArguments, + message: errorMessage, + newStatus: RuleExecutionStatus['partial failure'], + }); + wroteWarningStatus = true; } let hasError = false; const { tuples, remainingGap } = getRuleRangeTuples({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index c5b3e98c4c44e..f56d1d83eb873 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -207,7 +207,7 @@ describe('create_rules_stream_from_ndjson', () => { read() { this.push(getSampleAsNdjson(sample1)); this.push(getSampleAsNdjson(sample2)); - this.push('{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n'); + this.push('{"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0}\n'); this.push(null); }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index 0c2d81c18646b..d4357c45fd373 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -21,7 +21,8 @@ import { } from '../../../../common/detection_engine/schemas/request/import_rules_schema'; import { parseNdjsonStrings, - filterExportedCounts, + filterExportedRulesCounts, + filterExceptions, createLimitStream, } from '../../../utils/read_stream/create_stream_from_ndjson'; @@ -59,7 +60,8 @@ export const createRulesStreamFromNdJson = (ruleLimit: number) => { return [ createSplitStream('\n'), parseNdjsonStrings(), - filterExportedCounts(), + filterExportedRulesCounts(), + filterExceptions(), validateRules(), createLimitStream(ruleLimit), createConcatStream([]), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index 92e4f0bbb4a5e..80df4c94971cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -17,9 +17,12 @@ import { getListArrayMock } from '../../../../common/detection_engine/schemas/ty import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; import { loggingSystemMock } from 'src/core/server/mocks'; import { requestContextMock } from '../routes/__mocks__/request_context'; +const exceptionsClient = getExceptionListClientMock(); + describe.each([ ['Legacy', false], ['RAC', true], @@ -49,6 +52,7 @@ describe.each([ const exports = await getExportAll( rulesClient, + exceptionsClient, clients.savedObjectsClient, logger, isRuleRegistryEnabled @@ -97,7 +101,13 @@ describe.each([ exceptions_list: getListArrayMock(), }); expect(detailsJson).toEqual({ - exported_count: 1, + exported_exception_list_count: 0, + exported_exception_list_item_count: 0, + exported_rules_count: 1, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, missing_rules: [], missing_rules_count: 0, }); @@ -116,13 +126,16 @@ describe.each([ const exports = await getExportAll( rulesClient, + exceptionsClient, clients.savedObjectsClient, logger, isRuleRegistryEnabled ); expect(exports).toEqual({ rulesNdjson: '', - exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', + exportDetails: + '{"exported_rules_count":0,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n', + exceptionLists: '', }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts index cbbda5df7e2bf..c0389de766ea5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts @@ -8,35 +8,44 @@ import { transformDataToNdjson } from '@kbn/securitysolution-utils'; import { Logger } from 'src/core/server'; +import { ExceptionListClient } from '../../../../../lists/server'; import { RulesClient, AlertServices } from '../../../../../alerting/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { transformAlertsToRules } from '../routes/rules/utils'; +import { getRuleExceptionsForExport } from './get_export_rule_exceptions'; // eslint-disable-next-line no-restricted-imports import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; export const getExportAll = async ( rulesClient: RulesClient, + exceptionsClient: ExceptionListClient | undefined, savedObjectsClient: AlertServices['savedObjectsClient'], logger: Logger, isRuleRegistryEnabled: boolean ): Promise<{ rulesNdjson: string; exportDetails: string; + exceptionLists: string | null; }> => { const ruleAlertTypes = await getNonPackagedRules({ rulesClient, isRuleRegistryEnabled }); const alertIds = ruleAlertTypes.map((rule) => rule.id); + + // Gather actions const legacyActions = await legacyGetBulkRuleActionsSavedObject({ alertIds, savedObjectsClient, logger, }); - const rules = transformAlertsToRules(ruleAlertTypes, legacyActions); - // We do not support importing/exporting actions. When we do, delete this line of code - const rulesWithoutActions = rules.map((rule) => ({ ...rule, actions: [] })); - const rulesNdjson = transformDataToNdjson(rulesWithoutActions); - const exportDetails = getExportDetailsNdjson(rules); - return { rulesNdjson, exportDetails }; + + // Gather exceptions + const exceptions = rules.flatMap((rule) => rule.exceptions_list ?? []); + const { exportData: exceptionLists, exportDetails: exceptionDetails } = + await getRuleExceptionsForExport(exceptions, exceptionsClient); + + const rulesNdjson = transformDataToNdjson(rules); + const exportDetails = getExportDetailsNdjson(rules, [], exceptionDetails); + return { rulesNdjson, exportDetails, exceptionLists }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 961f2c6a41866..7aa55a8163e1a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -16,6 +16,9 @@ import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; + +const exceptionsClient = getExceptionListClientMock(); import { loggingSystemMock } from 'src/core/server/mocks'; import { requestContextMock } from '../routes/__mocks__/request_context'; @@ -42,6 +45,7 @@ describe.each([ const objects = [{ rule_id: 'rule-1' }]; const exports = await getExportByObjectIds( rulesClient, + exceptionsClient, clients.savedObjectsClient, objects, logger, @@ -94,7 +98,13 @@ describe.each([ exceptions_list: getListArrayMock(), }, exportDetails: { - exported_count: 1, + exported_exception_list_count: 0, + exported_exception_list_item_count: 0, + exported_rules_count: 1, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, missing_rules: [], missing_rules_count: 0, }, @@ -119,6 +129,7 @@ describe.each([ const objects = [{ rule_id: 'rule-1' }]; const exports = await getExportByObjectIds( rulesClient, + exceptionsClient, clients.savedObjectsClient, objects, logger, @@ -127,7 +138,8 @@ describe.each([ expect(exports).toEqual({ rulesNdjson: '', exportDetails: - '{"exported_count":0,"missing_rules":[{"rule_id":"rule-1"}],"missing_rules_count":1}\n', + '{"exported_rules_count":0,"missing_rules":[{"rule_id":"rule-1"}],"missing_rules_count":1,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0}\n', + exceptionLists: '', }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 8233fe6d4948c..81295c9197644 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -9,6 +9,7 @@ import { chunk } from 'lodash'; import { transformDataToNdjson } from '@kbn/securitysolution-utils'; import { Logger } from 'src/core/server'; +import { ExceptionListClient } from '../../../../../lists/server'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { RulesClient, AlertServices } from '../../../../../alerting/server'; @@ -18,6 +19,7 @@ import { isAlertType } from '../rules/types'; import { transformAlertToRule } from '../routes/rules/utils'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; +import { getRuleExceptionsForExport } from './get_export_rule_exceptions'; // eslint-disable-next-line no-restricted-imports import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; @@ -40,6 +42,7 @@ export interface RulesErrors { export const getExportByObjectIds = async ( rulesClient: RulesClient, + exceptionsClient: ExceptionListClient | undefined, savedObjectsClient: AlertServices['savedObjectsClient'], objects: Array<{ rule_id: string }>, logger: Logger, @@ -47,6 +50,7 @@ export const getExportByObjectIds = async ( ): Promise<{ rulesNdjson: string; exportDetails: string; + exceptionLists: string | null; }> => { const rulesAndErrors = await getRulesFromObjects( rulesClient, @@ -56,9 +60,19 @@ export const getExportByObjectIds = async ( isRuleRegistryEnabled ); + // Retrieve exceptions + const exceptions = rulesAndErrors.rules.flatMap((rule) => rule.exceptions_list ?? []); + const { exportData: exceptionLists, exportDetails: exceptionDetails } = + await getRuleExceptionsForExport(exceptions, exceptionsClient); + const rulesNdjson = transformDataToNdjson(rulesAndErrors.rules); - const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules); - return { rulesNdjson, exportDetails }; + const exportDetails = getExportDetailsNdjson( + rulesAndErrors.rules, + rulesAndErrors.missingRules, + exceptionDetails + ); + + return { rulesNdjson, exportDetails, exceptionLists }; }; export const getRulesFromObjects = async ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts index f4d50524d27b4..171233a861466 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.test.ts @@ -20,7 +20,7 @@ describe('getExportDetailsNdjson', () => { const details = getExportDetailsNdjson([rule]); const reParsed = JSON.parse(details); expect(reParsed).toEqual({ - exported_count: 1, + exported_rules_count: 1, missing_rules: [], missing_rules_count: 0, }); @@ -31,7 +31,7 @@ describe('getExportDetailsNdjson', () => { const details = getExportDetailsNdjson([], [missingRule]); const reParsed = JSON.parse(details); expect(reParsed).toEqual({ - exported_count: 0, + exported_rules_count: 0, missing_rules: [{ rule_id: 'rule-1' }], missing_rules_count: 1, }); @@ -49,7 +49,7 @@ describe('getExportDetailsNdjson', () => { const details = getExportDetailsNdjson([rule1, rule2], [missingRule1, missingRule2]); const reParsed = JSON.parse(details); expect(reParsed).toEqual({ - exported_count: 2, + exported_rules_count: 2, missing_rules: [missingRule1, missingRule2], missing_rules_count: 2, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts index 7f9ec77e9df79..429bf4f2926bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_details_ndjson.ts @@ -9,12 +9,14 @@ import { RulesSchema } from '../../../../common/detection_engine/schemas/respons export const getExportDetailsNdjson = ( rules: Array>, - missingRules: Array<{ rule_id: string }> = [] + missingRules: Array<{ rule_id: string }> = [], + extraMeta: Record = {} ): string => { const stringified = JSON.stringify({ - exported_count: rules.length, + exported_rules_count: rules.length, missing_rules: missingRules, missing_rules_count: missingRules.length, + ...extraMeta, }); return `${stringified}\n`; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.test.ts new file mode 100644 index 0000000000000..dd7e59c74601c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; + +import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; +import { + getRuleExceptionsForExport, + getExportableExceptions, + getDefaultExportDetails, +} from './get_export_rule_exceptions'; +import { + getListArrayMock, + getListMock, +} from '../../../../common/detection_engine/schemas/types/lists.mock'; + +describe('get_export_rule_exceptions', () => { + describe('getRuleExceptionsForExport', () => { + test('it returns empty exceptions array if no rules have exceptions associated', async () => { + const { exportData, exportDetails } = await getRuleExceptionsForExport( + [], + getExceptionListClientMock() + ); + + expect(exportData).toEqual(''); + expect(exportDetails).toEqual(getDefaultExportDetails()); + }); + + test('it returns stringified exceptions ready for export', async () => { + const { exportData } = await getRuleExceptionsForExport( + [getListMock()], + getExceptionListClientMock() + ); + + expect(exportData).toEqual('exportString'); + }); + + test('it does not return a global endpoint list', async () => { + const { exportData } = await getRuleExceptionsForExport( + [ + { + id: ENDPOINT_LIST_ID, + list_id: ENDPOINT_LIST_ID, + namespace_type: 'agnostic', + type: 'endpoint', + }, + ], + getExceptionListClientMock() + ); + + expect(exportData).toEqual(''); + }); + }); + + describe('getExportableExceptions', () => { + test('it returns stringified exception lists and items', async () => { + // This rule has 2 exception lists tied to it + const { exportData } = await getExportableExceptions( + getListArrayMock(), + getExceptionListClientMock() + ); + + expect(exportData).toEqual('exportStringexportString'); + }); + + test('it throws error if error occurs in getting exceptions', async () => { + const exceptionsClient = getExceptionListClientMock(); + exceptionsClient.exportExceptionListAndItems = jest.fn().mockRejectedValue(new Error('oops')); + // This rule has 2 exception lists tied to it + await expect(async () => { + await getExportableExceptions(getListArrayMock(), exceptionsClient); + }).rejects.toThrowErrorMatchingInlineSnapshot(`"oops"`); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.ts new file mode 100644 index 0000000000000..719649d35c0f0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_rule_exceptions.ts @@ -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 { chunk } from 'lodash/fp'; +import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; + +import { + ExceptionListClient, + ExportExceptionListAndItemsReturn, +} from '../../../../../lists/server'; + +const NON_EXPORTABLE_LIST_IDS = [ENDPOINT_LIST_ID]; +export const EXCEPTIONS_EXPORT_CHUNK_SIZE = 50; + +export const getRuleExceptionsForExport = async ( + exceptions: ListArray, + exceptionsListClient: ExceptionListClient | undefined +): Promise => { + if (exceptionsListClient != null) { + const exceptionsWithoutUnexportableLists = exceptions.filter( + ({ list_id: listId }) => !NON_EXPORTABLE_LIST_IDS.includes(listId) + ); + return getExportableExceptions(exceptionsWithoutUnexportableLists, exceptionsListClient); + } else { + return { exportData: '', exportDetails: getDefaultExportDetails() }; + } +}; + +export const getExportableExceptions = async ( + exceptions: ListArray, + exceptionsListClient: ExceptionListClient +): Promise => { + let exportString = ''; + const exportDetails = getDefaultExportDetails(); + + const exceptionChunks = chunk(EXCEPTIONS_EXPORT_CHUNK_SIZE, exceptions); + for await (const exceptionChunk of exceptionChunks) { + const promises = createPromises(exceptionsListClient, exceptionChunk); + + const responses = await Promise.all(promises); + + for (const res of responses) { + if (res != null) { + const { + exportDetails: { + exported_exception_list_count: exportedExceptionListCount, + exported_exception_list_item_count: exportedExceptionListItemCount, + }, + exportData, + } = res; + + exportDetails.exported_exception_list_count = + exportDetails.exported_exception_list_count + exportedExceptionListCount; + + exportDetails.exported_exception_list_item_count = + exportDetails.exported_exception_list_item_count + exportedExceptionListItemCount; + + exportString = `${exportString}${exportData}`; + } + } + } + + return { + exportDetails, + exportData: exportString, + }; +}; + +/** + * Creates promises of the rules and returns them. + * @param exceptionsListClient Exception Lists client + * @param exceptions The rules to apply the update for + * @returns Promise of export ready exceptions. + */ +export const createPromises = ( + exceptionsListClient: ExceptionListClient, + exceptions: ListArray +): Array> => { + return exceptions.map>( + async ({ id, list_id: listId, namespace_type: namespaceType }) => { + return exceptionsListClient.exportExceptionListAndItems({ + id, + listId, + namespaceType, + }); + } + ); +}; + +export const getDefaultExportDetails = () => ({ + exported_exception_list_count: 0, + exported_exception_list_item_count: 0, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 6a84776ccee5d..f9a2f5cfc0bfe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -263,7 +263,8 @@ describe('signal_rule_alert_type', () => { 2, expect.objectContaining({ newStatus: RuleExecutionStatus['partial failure'], - message: 'Missing required read privileges on the following indices: ["some*"]', + message: + 'This rule may not have the required read privileges to the following indices/index patterns: ["some*"]', }) ); }); @@ -293,7 +294,7 @@ describe('signal_rule_alert_type', () => { expect.objectContaining({ newStatus: RuleExecutionStatus['partial failure'], message: - 'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]', + 'This rule may not have the required read privileges to the following indices/index patterns: ["myfa*","some*"]', }) ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 90220814fb928..0b1524a5682ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -8,12 +8,9 @@ import { Logger, SavedObject } from 'src/core/server'; import isEmpty from 'lodash/isEmpty'; -import { chain, tryCatch } from 'fp-ts/lib/TaskEither'; -import { flow } from 'fp-ts/lib/function'; import * as t from 'io-ts'; import { validateNonExact, parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; -import { toError, toPromise } from '@kbn/securitysolution-list-api'; import { SIGNALS_ID, @@ -191,53 +188,44 @@ export const signalRulesAlertType = ({ index, experimentalFeatures, }); - const [privileges, timestampFieldCaps] = await Promise.all([ - checkPrivileges(services, inputIndices), - services.scopedClusterClient.asCurrentUser.fieldCaps({ + const privileges = await checkPrivileges(services, inputIndices); + + wroteWarningStatus = await hasReadIndexPrivileges({ + ...basicLogArguments, + privileges, + logger, + buildRuleMessage, + ruleStatusClient, + }); + + if (!wroteWarningStatus) { + const timestampFieldCaps = await services.scopedClusterClient.asCurrentUser.fieldCaps({ index, fields: hasTimestampOverride ? ['@timestamp', timestampOverride as string] : ['@timestamp'], include_unmapped: true, - }), - ]); - - wroteWarningStatus = await flow( - () => - tryCatch( - () => - hasReadIndexPrivileges({ - ...basicLogArguments, - privileges, - logger, - buildRuleMessage, - ruleStatusClient, - }), - toError - ), - chain((wroteStatus) => - tryCatch( - () => - hasTimestampFields({ - ...basicLogArguments, - wroteStatus: wroteStatus as boolean, - timestampField: hasTimestampOverride - ? (timestampOverride as string) - : '@timestamp', - timestampFieldCapsResponse: timestampFieldCaps, - inputIndices, - ruleStatusClient, - logger, - buildRuleMessage, - }), - toError - ) - ), - toPromise - )(); + }); + wroteWarningStatus = await hasTimestampFields({ + ...basicLogArguments, + timestampField: hasTimestampOverride ? (timestampOverride as string) : '@timestamp', + timestampFieldCapsResponse: timestampFieldCaps, + inputIndices, + ruleStatusClient, + logger, + buildRuleMessage, + }); + } } } catch (exc) { - logger.error(buildRuleMessage(`Check privileges failed to execute ${exc}`)); + const errorMessage = buildRuleMessage(`Check privileges failed to execute ${exc}`); + logger.error(errorMessage); + await ruleStatusClient.logStatusChange({ + ...basicLogArguments, + message: errorMessage, + newStatus: RuleExecutionStatus['partial failure'], + }); + wroteWarningStatus = true; } const { tuples, remainingGap } = getRuleRangeTuples({ logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index c202065176ff1..e2ef2cb6c841d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -66,8 +66,11 @@ const getTransformedHits = ( timestampOverride: TimestampOverrideOrUndefined, signalHistory: ThresholdSignalHistory ) => { + if (results.aggregations == null) { + return []; + } const aggParts = threshold.field.length - ? results.aggregations && getThresholdAggregationParts(results.aggregations) + ? getThresholdAggregationParts(results.aggregations) : { field: null, index: 0, @@ -132,8 +135,7 @@ const getTransformedHits = ( }; return getCombinations( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (results.aggregations![aggParts.name] as { buckets: TermAggregationBucket[] }).buckets, + (results.aggregations[aggParts.name] as { buckets: TermAggregationBucket[] }).buckets, 0, aggParts.field ).reduce((acc: Array>, bucket) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 0e50db97d1256..ce2b15a46ef6f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -781,7 +781,6 @@ describe('utils', () => { }; mockLogger.error.mockClear(); const res = await hasTimestampFields({ - wroteStatus: false, timestampField, ruleName: 'myfakerulename', // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -825,7 +824,6 @@ describe('utils', () => { }; mockLogger.error.mockClear(); const res = await hasTimestampFields({ - wroteStatus: false, timestampField, ruleName: 'myfakerulename', // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -855,7 +853,6 @@ describe('utils', () => { }; mockLogger.error.mockClear(); const res = await hasTimestampFields({ - wroteStatus: false, timestampField, ruleName: 'Endpoint Security', // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -885,7 +882,6 @@ describe('utils', () => { }; mockLogger.error.mockClear(); const res = await hasTimestampFields({ - wroteStatus: false, timestampField, ruleName: 'NOT Endpoint Security', // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 79b36cf62573a..0a3eda70bbd87 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -115,34 +115,15 @@ export const hasReadIndexPrivileges = async (args: { } = args; const indexNames = Object.keys(privileges.index); - const [indexesWithReadPrivileges, indexesWithNoReadPrivileges] = partition( + const [, indexesWithNoReadPrivileges] = partition( indexNames, (indexName) => privileges.index[indexName].read ); - if (indexesWithReadPrivileges.length > 0 && indexesWithNoReadPrivileges.length > 0) { + if (indexesWithNoReadPrivileges.length > 0) { // some indices have read privileges others do not. // set a warning status - const errorString = `Missing required read privileges on the following indices: ${JSON.stringify( - indexesWithNoReadPrivileges - )}`; - logger.error(buildRuleMessage(errorString)); - await ruleStatusClient.logStatusChange({ - message: errorString, - ruleId, - ruleName, - ruleType, - spaceId, - newStatus: RuleExecutionStatus['partial failure'], - }); - return true; - } else if ( - indexesWithReadPrivileges.length === 0 && - indexesWithNoReadPrivileges.length === indexNames.length - ) { - // none of the indices had read privileges so set the status to failed - // since we can't search on any indices we do not have read privileges on - const errorString = `This rule may not have the required read privileges to the following indices: ${JSON.stringify( + const errorString = `This rule may not have the required read privileges to the following indices/index patterns: ${JSON.stringify( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); @@ -160,7 +141,6 @@ export const hasReadIndexPrivileges = async (args: { }; export const hasTimestampFields = async (args: { - wroteStatus: boolean; timestampField: string; ruleName: string; // any is derived from here @@ -176,7 +156,6 @@ export const hasTimestampFields = async (args: { buildRuleMessage: BuildRuleMessage; }): Promise => { const { - wroteStatus, timestampField, ruleName, timestampFieldCapsResponse, @@ -189,7 +168,7 @@ export const hasTimestampFields = async (args: { buildRuleMessage, } = args; - if (!wroteStatus && isEmpty(timestampFieldCapsResponse.body.indices)) { + if (isEmpty(timestampFieldCapsResponse.body.indices)) { const errorString = `This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ${JSON.stringify( inputIndices )} was found. This warning will continue to appear until a matching index is created or this rule is de-activated. ${ @@ -208,10 +187,9 @@ export const hasTimestampFields = async (args: { }); return true; } else if ( - !wroteStatus && - (isEmpty(timestampFieldCapsResponse.body.fields) || - timestampFieldCapsResponse.body.fields[timestampField] == null || - timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices != null) + isEmpty(timestampFieldCapsResponse.body.fields) || + timestampFieldCapsResponse.body.fields[timestampField] == null || + timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices != null ) { // if there is a timestamp override and the unmapped array for the timestamp override key is not empty, // warning @@ -236,7 +214,7 @@ export const hasTimestampFields = async (args: { }); return true; } - return wroteStatus; + return false; }; export const checkPrivileges = async ( @@ -257,6 +235,7 @@ export const checkPrivilegesFromEsClient = async ( index: [ { names: indices ?? [], + allow_restricted_indices: true, privileges: ['read'], }, ], diff --git a/x-pack/plugins/security_solution/server/lib/framework/types.ts b/x-pack/plugins/security_solution/server/lib/framework/types.ts index eceff4b35f74f..3ecd2adf00242 100644 --- a/x-pack/plugins/security_solution/server/lib/framework/types.ts +++ b/x-pack/plugins/security_solution/server/lib/framework/types.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { KibanaRequest } from '../../../../../../src/core/server'; +import { KibanaRequest, RequestHandlerContext } from '../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../security/common/model'; -import type { SecuritySolutionRequestHandlerContext } from '../../types'; export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); export interface FrameworkRequest extends Pick { [internalFrameworkRequest]: KibanaRequest; - context: SecuritySolutionRequestHandlerContext; + context: RequestHandlerContext; user: AuthenticatedUser | null; } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts index 05f3b5373a8de..8bf5213d6a47f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts @@ -32,17 +32,12 @@ const notes = [ const existingNoteIds = undefined; const isImmutable = true; -jest.mock('moment', () => { - const mockMoment = { - toISOString: jest - .fn() - .mockReturnValueOnce('2020-11-03T11:37:31.655Z') - .mockReturnValue('2020-11-04T11:37:31.655Z'), - subtract: jest.fn(), - }; - mockMoment.subtract.mockReturnValue(mockMoment); - return jest.fn().mockReturnValue(mockMoment); -}); +// System under test uses moment.js under the hood, so we need to mock time. +// Mocking moment via jest.mock('moment') breaks imports of moment in other files. +// Instead, we simply mock Date.now() via jest API and moment starts using it. +// This affects all the tests in this file and doesn't affect tests in other files. +// https://jestjs.io/docs/timer-mocks +jest.useFakeTimers('modern').setSystemTime(new Date('2020-11-04T11:37:31.655Z')); jest.mock('../../../saved_object/timelines', () => ({ persistTimeline: jest.fn().mockResolvedValue({ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts index be086732ddcd0..91f8e4153a63b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts @@ -12,15 +12,14 @@ import { Readable } from 'stream'; import { createListStream } from '@kbn/utils'; import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'src/core/server'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { SetupPlugins, StartPlugins } from '../../../plugin'; -import type { SecuritySolutionRequestHandlerContext } from '../../../types'; import { FrameworkRequest } from '../../framework'; export const buildFrameworkRequest = async ( - context: SecuritySolutionRequestHandlerContext, + context: RequestHandlerContext, security: StartPlugins['security'] | SetupPlugins['security'] | undefined, request: KibanaRequest ): Promise => { @@ -30,7 +29,7 @@ export const buildFrameworkRequest = async ( return set( 'user', user, - set( + set( 'context.core.savedObjects.client', savedObjectsClient, request diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0f908d7db8e05..f2aa4927a7688 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -9,45 +9,15 @@ import { Observable } from 'rxjs'; import LRU from 'lru-cache'; import { estypes } from '@elastic/elasticsearch'; -import { - CoreSetup, - CoreStart, - Logger, - Plugin as IPlugin, - PluginInitializerContext, - SavedObjectsClient, -} from '../../../../src/core/server'; -import { - PluginSetup as DataPluginSetup, - PluginStart as DataPluginStart, -} from '../../../../src/plugins/data/server'; -import { - UsageCollectionSetup, - UsageCounter, -} from '../../../../src/plugins/usage_collection/server'; -import { - PluginSetupContract as AlertingSetup, - PluginStartContract as AlertPluginStartContract, -} from '../../alerting/server'; -import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; +import { Logger, SavedObjectsClient } from '../../../../src/core/server'; +import { UsageCounter } from '../../../../src/plugins/usage_collection/server'; -import { PluginStartContract as CasesPluginStartContract } from '../../cases/server'; import { ECS_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; -import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server'; -import { - IRuleDataClient, - RuleRegistryPluginSetupContract, - RuleRegistryPluginStartContract, - Dataset, -} from '../../rule_registry/server'; -import { PluginSetupContract as FeaturesSetup } from '../../features/server'; -import { MlPluginSetup as MlSetup } from '../../ml/server'; +import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; +import { IRuleDataClient, Dataset } from '../../rule_registry/server'; import { ListPluginSetup } from '../../lists/server'; -import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; -import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; -import { ILicense, LicensingPluginStart } from '../../licensing/server'; -import { FleetStartContract } from '../../fleet/server'; -import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; +import { ILicense } from '../../licensing/server'; + import { createEqlAlertType, createIndicatorMatchAlertType, @@ -70,7 +40,6 @@ import { SIGNALS_ID, LEGACY_NOTIFICATIONS_ID, QUERY_RULE_TYPE_ID, - DEFAULT_SPACE_ID, INDICATOR_RULE_TYPE_ID, ML_RULE_TYPE_ID, EQL_RULE_TYPE_ID, @@ -89,18 +58,13 @@ import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; import { TelemetryEventsSender } from './lib/telemetry/sender'; import { TelemetryReceiver } from './lib/telemetry/receiver'; -import { - TelemetryPluginStart, - TelemetryPluginSetup, -} from '../../../../src/plugins/telemetry/server'; import { licenseService } from './lib/license'; import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; -import { parseExperimentalConfigValue } from '../common/experimental_features'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; import aadFieldConversion from './lib/detection_engine/routes/index/signal_aad_mapping.json'; import { alertsFieldMap } from './lib/detection_engine/rule_types/field_maps/alerts'; import { rulesFieldMap } from './lib/detection_engine/rule_types/field_maps/rules'; -import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client'; +import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider'; import { getKibanaPrivilegesFeaturePrivileges, getCasesKibanaFeature } from './features'; import { EndpointMetadataService } from './endpoint/services/metadata'; import { CreateRuleOptions } from './lib/detection_engine/rule_types/types'; @@ -109,50 +73,28 @@ import { legacyRulesNotificationAlertType } from './lib/detection_engine/notific // eslint-disable-next-line no-restricted-imports import { legacyIsNotificationAlertExecutor } from './lib/detection_engine/notifications/legacy_types'; import { createSecurityRuleTypeWrapper } from './lib/detection_engine/rule_types/create_security_rule_type_wrapper'; -import { IEventLogClientService, IEventLogService } from '../../event_log/server'; -import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider'; -export interface SetupPlugins { - alerting: AlertingSetup; - data: DataPluginSetup; - encryptedSavedObjects?: EncryptedSavedObjectsSetup; - eventLog: IEventLogService; - features: FeaturesSetup; - lists?: ListPluginSetup; - ml?: MlSetup; - ruleRegistry: RuleRegistryPluginSetupContract; - security?: SecuritySetup; - spaces?: SpacesSetup; - taskManager?: TaskManagerSetupContract; - telemetry?: TelemetryPluginSetup; - usageCollection?: UsageCollectionSetup; -} - -export interface StartPlugins { - alerting: AlertPluginStartContract; - cases?: CasesPluginStartContract; - data: DataPluginStart; - eventLog: IEventLogClientService; - fleet?: FleetStartContract; - licensing: LicensingPluginStart; - ruleRegistry: RuleRegistryPluginStartContract; - security: SecurityPluginStart; - taskManager?: TaskManagerStartContract; - telemetry?: TelemetryPluginStart; -} +import { RequestContextFactory } from './request_context_factory'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginSetup {} +import type { + ISecuritySolutionPlugin, + SecuritySolutionPluginSetupDependencies, + SecuritySolutionPluginStartDependencies, + SecuritySolutionPluginCoreSetupDependencies, + SecuritySolutionPluginCoreStartDependencies, + SecuritySolutionPluginSetup, + SecuritySolutionPluginStart, + PluginInitializerContext, +} from './plugin_contract'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginStart {} +export { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract'; -export class Plugin implements IPlugin { - private readonly logger: Logger; +export class Plugin implements ISecuritySolutionPlugin { + private readonly pluginContext: PluginInitializerContext; private readonly config: ConfigType; - private context: PluginInitializerContext; - private appClientFactory: AppClientFactory; - private setupPlugins?: SetupPlugins; + private readonly logger: Logger; + private readonly appClientFactory: AppClientFactory; + private readonly endpointAppContextService = new EndpointAppContextService(); private readonly telemetryReceiver: TelemetryReceiver; private readonly telemetryEventsSender: TelemetryEventsSender; @@ -167,10 +109,11 @@ export class Plugin implements IPlugin({ max: 3, maxAge: 1000 * 60 * 5 }); this.telemetryEventsSender = new TelemetryEventsSender(this.logger); @@ -179,26 +122,47 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { + public setup( + core: SecuritySolutionPluginCoreSetupDependencies, + plugins: SecuritySolutionPluginSetupDependencies + ): SecuritySolutionPluginSetup { this.logger.debug('plugin setup'); - this.setupPlugins = plugins; - const config = this.config; - const globalConfig = this.context.config.legacy.get(); + const { pluginContext, config, logger, appClientFactory } = this; + const experimentalFeatures = config.experimentalFeatures; + + appClientFactory.setup({ + getSpaceId: plugins.spaces?.spacesService?.getSpaceId, + config, + }); - const experimentalFeatures = parseExperimentalConfigValue(config.enableExperimental); initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings, experimentalFeatures); + + const eventLogService = plugins.eventLog; + registerEventLogProvider(eventLogService); + + const requestContextFactory = new RequestContextFactory({ config, core, plugins }); + const router = core.http.createRouter(); + core.http.registerRouteHandlerContext( + APP_ID, + (context, request) => requestContextFactory.create(context, request) + ); + const endpointContext: EndpointAppContext = { - logFactory: this.context.logger, + logFactory: pluginContext.logger, service: this.endpointAppContextService, config: (): Promise => Promise.resolve(config), experimentalFeatures, }; + this.endpointAppContextService.setup({ + securitySolutionRequestContextFactory: requestContextFactory, + }); + initUsageCollectors({ core, - kibanaIndex: globalConfig.kibana.index, + kibanaIndex: config.kibanaIndex, signalsIndex: config.signalsIndex, ml: plugins.ml, usageCollection: plugins.usageCollection, @@ -206,29 +170,6 @@ export class Plugin implements IPlugin(); - core.http.registerRouteHandlerContext( - APP_ID, - (context, request, response) => ({ - getAppClient: () => this.appClientFactory.create(request), - getSpaceId: () => plugins.spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID, - getExecutionLogClient: () => - new RuleExecutionLogClient({ - savedObjectsClient: context.core.savedObjects.client, - eventLogService, - underlyingClient: config.ruleExecutionLog.underlyingClient, - }), - }) - ); - - this.appClientFactory.setup({ - getSpaceId: plugins.spaces?.spacesService?.getSpaceId, - config, - }); - // TODO: Once we are past experimental phase this check can be removed along with legacy registration of rules const isRuleRegistryEnabled = experimentalFeatures.ruleRegistryEnabled; @@ -265,9 +206,9 @@ export class Plugin implements IPlugin; + +export type SecuritySolutionPluginCoreStartDependencies = CoreStart; + +export type ISecuritySolutionPlugin = Plugin< + SecuritySolutionPluginSetup, + SecuritySolutionPluginStart, + SecuritySolutionPluginSetupDependencies, + SecuritySolutionPluginStartDependencies +>; + +export type { + PluginInitializerContext, + // Legacy type identifiers left for compatibility with the rest of the code: + SecuritySolutionPluginSetupDependencies as SetupPlugins, + SecuritySolutionPluginStartDependencies as StartPlugins, + SecuritySolutionPluginSetup as PluginSetup, + SecuritySolutionPluginStart as PluginStart, +}; diff --git a/x-pack/plugins/security_solution/server/request_context_factory.mock.ts b/x-pack/plugins/security_solution/server/request_context_factory.mock.ts new file mode 100644 index 0000000000000..5621ac8fb26ab --- /dev/null +++ b/x-pack/plugins/security_solution/server/request_context_factory.mock.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { requestContextMock } from './lib/detection_engine/routes/__mocks__'; +import { IRequestContextFactory } from './request_context_factory'; + +export const requestContextFactoryMock = { + create: (): jest.Mocked => ({ + create: jest.fn((context, request) => { + const fullContext = requestContextMock.create(); + const securitySolutionContext = fullContext.securitySolution; + return Promise.resolve(securitySolutionContext); + }), + }), +}; + +export const RequestContextFactoryMock = jest + .fn, []>() + .mockImplementation(requestContextFactoryMock.create); diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts new file mode 100644 index 0000000000000..c2e622bc495c9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -0,0 +1,90 @@ +/* + * 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 { KibanaRequest, RequestHandlerContext } from 'kibana/server'; +import { ExceptionListClient } from '../../lists/server'; + +import { DEFAULT_SPACE_ID } from '../common/constants'; +import { AppClientFactory } from './client'; +import { ConfigType } from './config'; +import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client'; +import { buildFrameworkRequest } from './lib/timeline/utils/common'; +import { + SecuritySolutionPluginCoreSetupDependencies, + SecuritySolutionPluginSetupDependencies, +} from './plugin_contract'; +import { SecuritySolutionApiRequestHandlerContext } from './types'; + +export interface IRequestContextFactory { + create( + context: RequestHandlerContext, + request: KibanaRequest + ): Promise; +} + +interface ConstructorOptions { + config: ConfigType; + core: SecuritySolutionPluginCoreSetupDependencies; + plugins: SecuritySolutionPluginSetupDependencies; +} + +export class RequestContextFactory implements IRequestContextFactory { + private readonly appClientFactory: AppClientFactory; + + constructor(private readonly options: ConstructorOptions) { + this.appClientFactory = new AppClientFactory(); + } + + public async create( + context: RequestHandlerContext, + request: KibanaRequest + ): Promise { + const { options, appClientFactory } = this; + const { config, plugins } = options; + const { lists, ruleRegistry, security, spaces } = plugins; + + appClientFactory.setup({ + getSpaceId: plugins.spaces?.spacesService?.getSpaceId, + config, + }); + + const frameworkRequest = await buildFrameworkRequest(context, security, request); + + return { + core: context.core, + + getConfig: () => config, + + getFrameworkRequest: () => frameworkRequest, + + getAppClient: () => appClientFactory.create(request), + + getSpaceId: () => spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID, + + getRuleDataService: () => ruleRegistry.ruleDataService, + + getExecutionLogClient: () => + new RuleExecutionLogClient({ + savedObjectsClient: context.core.savedObjects.client, + eventLogService: plugins.eventLog, + underlyingClient: config.ruleExecutionLog.underlyingClient, + }), + + getExceptionListClient: () => { + if (!lists) { + return null; + } + + const username = security?.authc.getCurrentUser(request)?.username || 'elastic'; + return new ExceptionListClient({ + savedObjectsClient: context.core.savedObjects.client, + user: username, + }); + }, + }; + } +} diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 148580d5c4477..9d31684907f86 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -6,7 +6,6 @@ */ import { Logger } from 'src/core/server'; -import { RuleDataPluginService } from '../../../rule_registry/server'; import { SecuritySolutionPluginRouter } from '../types'; @@ -67,7 +66,6 @@ export const initRoutes = ( hasEncryptionKey: boolean, security: SetupPlugins['security'], ml: SetupPlugins['ml'], - ruleDataService: RuleDataPluginService, logger: Logger, isRuleRegistryEnabled: boolean ) => { @@ -85,7 +83,7 @@ export const initRoutes = ( // TODO: pass isRuleRegistryEnabled to all relevant routes - addPrepackedRulesRoute(router, config, security, isRuleRegistryEnabled); + addPrepackedRulesRoute(router); getPrepackagedRulesStatusRoute(router, config, security, isRuleRegistryEnabled); createRulesBulkRoute(router, ml, isRuleRegistryEnabled); updateRulesBulkRoute(router, ml, isRuleRegistryEnabled); @@ -127,7 +125,7 @@ export const initRoutes = ( // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index // All REST index creation, policy management for spaces - createIndexRoute(router, ruleDataService, config); + createIndexRoute(router); readIndexRoute(router, config); deleteIndexRoute(router); diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 7822a5b8ba3c5..84643a329573b 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -6,28 +6,35 @@ */ import type { IRouter, RequestHandlerContext } from 'src/core/server'; -import type { ListsApiRequestHandlerContext } from '../../lists/server'; -import type { LicensingApiRequestHandlerContext } from '../../licensing/server'; +import type { ActionsApiRequestHandlerContext } from '../../actions/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; +import type { LicensingApiRequestHandlerContext } from '../../licensing/server'; +import type { ListsApiRequestHandlerContext, ExceptionListClient } from '../../lists/server'; +import type { IRuleDataService } from '../../rule_registry/server'; import { AppClient } from './client'; -import { RuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/rule_execution_log_client'; -import type { ActionsApiRequestHandlerContext } from '../../actions/server'; +import { ConfigType } from './config'; +import { IRuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/types'; +import { FrameworkRequest } from './lib/framework'; export { AppClient }; -export interface AppRequestContext { +export interface SecuritySolutionApiRequestHandlerContext extends RequestHandlerContext { + getConfig: () => ConfigType; + getFrameworkRequest: () => FrameworkRequest; getAppClient: () => AppClient; getSpaceId: () => string; - getExecutionLogClient: () => RuleExecutionLogClient; + getRuleDataService: () => IRuleDataService; + getExecutionLogClient: () => IRuleExecutionLogClient; + getExceptionListClient: () => ExceptionListClient | null; } -export type SecuritySolutionRequestHandlerContext = RequestHandlerContext & { - securitySolution: AppRequestContext; - licensing: LicensingApiRequestHandlerContext; - alerting: AlertingApiRequestHandlerContext; +export interface SecuritySolutionRequestHandlerContext extends RequestHandlerContext { + securitySolution: SecuritySolutionApiRequestHandlerContext; actions: ActionsApiRequestHandlerContext; + alerting: AlertingApiRequestHandlerContext; + licensing: LicensingApiRequestHandlerContext; lists?: ListsApiRequestHandlerContext; -}; +} export type SecuritySolutionPluginRouter = IRouter; diff --git a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts index eb5abaee8cd3b..914c684fe8813 100644 --- a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts @@ -34,6 +34,18 @@ export const filterExportedCounts = (): Transform => { ); }; +export const filterExportedRulesCounts = (): Transform => { + return createFilterStream( + (obj) => obj != null && !has('exported_rules_count', obj) + ); +}; + +export const filterExceptions = (): Transform => { + return createFilterStream( + (obj) => obj != null && !has('list_id', obj) + ); +}; + // Adaptation from: saved_objects/import/create_limit_stream.ts export const createLimitStream = (limit: number): Transform => { let counter = 0; diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 5c87d58199df4..8f4d602e26461 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -1,4 +1,3 @@ - { "extends": "../../../tsconfig.base.json", "compilerOptions": { @@ -40,7 +39,7 @@ { "path": "../maps/tsconfig.json" }, { "path": "../ml/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, - { "path": "../security/tsconfig.json"}, - { "path": "../timelines/tsconfig.json"}, + { "path": "../security/tsconfig.json" }, + { "path": "../timelines/tsconfig.json" } ] } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 781ef74a872eb..c54512203677d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3722,7 +3722,6 @@ "indexPatternEditor.aliasLabel": "エイリアス", "indexPatternEditor.createIndex.noMatch": "名前は1つ以上のデータストリーム、インデックス、またはインデックスエイリアスと一致する必要があります。", "indexPatternEditor.createIndexPattern.emptyState.checkDataButton": "新規データを確認", - "indexPatternEditor.createIndexPattern.emptyState.createAnyway": "一部のインデックスは表示されない場合があります。{link}してください。", "indexPatternEditor.createIndexPattern.emptyState.createAnywayLink": "インデックスパターンを作成します", "indexPatternEditor.createIndexPattern.emptyState.haveData": "すでにデータがある場合", "indexPatternEditor.createIndexPattern.emptyState.integrationCardDescription": "さまざまなソースからデータを追加します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 92646cc1f9cd9..fbc65161a47f8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3755,7 +3755,6 @@ "indexPatternEditor.aliasLabel": "别名", "indexPatternEditor.createIndex.noMatch": "名称必须匹配一个或多个数据流、索引或索引别名。", "indexPatternEditor.createIndexPattern.emptyState.checkDataButton": "检查新数据", - "indexPatternEditor.createIndexPattern.emptyState.createAnyway": "部分索引可能已隐藏。仍然尝试{link}。", "indexPatternEditor.createIndexPattern.emptyState.createAnywayLink": "创建索引模式", "indexPatternEditor.createIndexPattern.emptyState.haveData": "假设您已有数据?", "indexPatternEditor.createIndexPattern.emptyState.integrationCardDescription": "从各种源添加数据。", diff --git a/x-pack/plugins/uptime/e2e/config.ts b/x-pack/plugins/uptime/e2e/config.ts index 70cc57247d490..c5d573afccd96 100644 --- a/x-pack/plugins/uptime/e2e/config.ts +++ b/x-pack/plugins/uptime/e2e/config.ts @@ -39,7 +39,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { '--csp.warnLegacyBrowsers=false', // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, - `--elasticsearch.ignoreVersionMismatch=true`, + `--elasticsearch.ignoreVersionMismatch=${process.env.CI ? 'false' : 'true'}`, `--uiSettings.overrides.theme:darkMode=true`, `--elasticsearch.username=kibana_system`, `--elasticsearch.password=changeme`, diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index aedb255b058be..5949339c1ba25 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -15,15 +15,10 @@ import './journeys'; export function playwrightRunTests() { return async ({ getService }: any) => { - try { - const result = await playwrightStart(getService); - - if (result && result.uptime.status !== 'succeeded') { - process.exit(1); - } - } catch (error) { - console.error('errors: ', error); - process.exit(1); + const result = await playwrightStart(getService); + + if (result && result.uptime.status !== 'succeeded') { + throw new Error('Tests failed'); } }; } @@ -42,7 +37,7 @@ async function playwrightStart(getService: any) { const res = await playwrightRun({ params: { kibanaUrl }, - playwrightOptions: { chromiumSandbox: false, timeout: 60 * 1000 }, + playwrightOptions: { headless: true, chromiumSandbox: false, timeout: 60 * 1000 }, }); console.log('Removing esArchiver...'); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 789953258750b..26f9e28101ea4 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -51,7 +51,7 @@ export function ActionMenuContent(): React.ReactElement { allSeries: [ { dataType: 'synthetics', - seriesType: 'area_stacked', + seriesType: 'area', selectedMetricField: 'monitor.duration.us', time: { from: dateRangeStart, to: dateRangeEnd }, breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts index 640db94028bc6..e457453a38f19 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts @@ -9,9 +9,9 @@ import { BrowserFields, ConfigKeys } from '../types'; import { Formatter, commonFormatters, + objectToJsonFormatter, arrayToJsonFormatter, stringToJsonFormatter, - objectToJsonFormatter, } from '../common/formatters'; import { tlsValueToYamlFormatter, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts index 34b937b80dad0..2c675b9f28804 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts @@ -14,7 +14,6 @@ import { } from '../common/normalizers'; import { defaultBrowserSimpleFields, defaultBrowserAdvancedFields } from '../contexts'; -import { tlsJsonToObjectNormalizer, tlsStringToObjectNormalizer } from '../tls/normalizers'; export type BrowserNormalizerMap = Record; @@ -42,33 +41,22 @@ export const browserNormalizers: BrowserNormalizerMap = { [ConfigKeys.PARAMS]: getBrowserNormalizer(ConfigKeys.PARAMS), [ConfigKeys.SCREENSHOTS]: getBrowserNormalizer(ConfigKeys.SCREENSHOTS), [ConfigKeys.SYNTHETICS_ARGS]: getBrowserJsonToJavascriptNormalizer(ConfigKeys.SYNTHETICS_ARGS), - [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: (fields) => - tlsJsonToObjectNormalizer( - fields?.[ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]?.value, - ConfigKeys.TLS_CERTIFICATE_AUTHORITIES - ), - [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: (fields) => - tlsJsonToObjectNormalizer( - fields?.[ConfigKeys.ZIP_URL_TLS_CERTIFICATE]?.value, - ConfigKeys.TLS_CERTIFICATE - ), - [ConfigKeys.ZIP_URL_TLS_KEY]: (fields) => - tlsJsonToObjectNormalizer(fields?.[ConfigKeys.ZIP_URL_TLS_KEY]?.value, ConfigKeys.TLS_KEY), - [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: (fields) => - tlsStringToObjectNormalizer( - fields?.[ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]?.value, - ConfigKeys.TLS_KEY_PASSPHRASE - ), - [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: (fields) => - tlsStringToObjectNormalizer( - fields?.[ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]?.value, - ConfigKeys.TLS_VERIFICATION_MODE - ), - [ConfigKeys.ZIP_URL_TLS_VERSION]: (fields) => - tlsJsonToObjectNormalizer( - fields?.[ConfigKeys.ZIP_URL_TLS_VERSION]?.value, - ConfigKeys.TLS_VERSION - ), + [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: getBrowserJsonToJavascriptNormalizer( + ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES + ), + [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: getBrowserJsonToJavascriptNormalizer( + ConfigKeys.ZIP_URL_TLS_CERTIFICATE + ), + [ConfigKeys.ZIP_URL_TLS_KEY]: getBrowserJsonToJavascriptNormalizer(ConfigKeys.ZIP_URL_TLS_KEY), + [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: getBrowserNormalizer( + ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE + ), + [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: getBrowserNormalizer( + ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE + ), + [ConfigKeys.ZIP_URL_TLS_VERSION]: getBrowserJsonToJavascriptNormalizer( + ConfigKeys.ZIP_URL_TLS_VERSION + ), [ConfigKeys.JOURNEY_FILTERS_MATCH]: getBrowserJsonToJavascriptNormalizer( ConfigKeys.JOURNEY_FILTERS_MATCH ), diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.test.tsx new file mode 100644 index 0000000000000..b19ca47ab76a5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.test.tsx @@ -0,0 +1,125 @@ +/* + * 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 { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { ScriptRecorderFields } from './script_recorder_fields'; +import { PolicyConfigContextProvider } from '../contexts'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'), + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +const onChange = jest.fn(); + +describe('', () => { + let file: File; + const testScript = 'step(() => {})'; + const WrappedComponent = ({ + isEditable = true, + script = '', + fileName = '', + }: { + isEditable?: boolean; + script?: string; + fileName?: string; + }) => { + return ( + + + + ); + }; + + beforeEach(() => { + jest.clearAllMocks(); + file = new File([testScript], 'samplescript.js', { type: 'text/javascript' }); + }); + + it('renders ScriptRecorderFields', () => { + const { getByText, queryByText } = render(); + + const downloadLink = getByText('Download the Elastic Synthetics Recorder'); + + expect(downloadLink).toBeInTheDocument(); + expect(downloadLink).toHaveAttribute('target', '_blank'); + + expect(queryByText('Show script')).not.toBeInTheDocument(); + expect(queryByText('Remove script')).not.toBeInTheDocument(); + }); + + it('handles uploading files', async () => { + const { getByTestId } = render(); + + const uploader = getByTestId('syntheticsFleetScriptRecorderUploader'); + + fireEvent.change(uploader, { + target: { files: [file] }, + }); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ scriptText: testScript, fileName: 'samplescript.js' }); + }); + }); + + it('shows user errors for invalid file types', async () => { + const { getByTestId, getByText } = render(); + file = new File(['journey(() => {})'], 'samplescript.js', { type: 'text/javascript' }); + + let uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement; + + fireEvent.change(uploader, { + target: { files: [file] }, + }); + + uploader = getByTestId('syntheticsFleetScriptRecorderUploader') as HTMLInputElement; + + await waitFor(() => { + expect(onChange).not.toBeCalled(); + expect( + getByText( + 'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.' + ) + ).toBeInTheDocument(); + }); + }); + + it('shows show script button when script is available', () => { + const { getByText, queryByText } = render(); + + const showScriptBtn = getByText('Show script'); + + expect(queryByText(testScript)).not.toBeInTheDocument(); + + fireEvent.click(showScriptBtn); + + expect(getByText(testScript)).toBeInTheDocument(); + }); + + it('shows show remove script button when script is available and isEditable is true', async () => { + const { getByText, getByTestId } = render( + + ); + + const showScriptBtn = getByText('Show script'); + fireEvent.click(showScriptBtn); + + expect(getByText(testScript)).toBeInTheDocument(); + + fireEvent.click(getByTestId('euiFlyoutCloseButton')); + + const removeScriptBtn = getByText('Remove script'); + + fireEvent.click(removeScriptBtn); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ scriptText: '', fileName: '' }); + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.tsx new file mode 100644 index 0000000000000..9b99b4094e63b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/script_recorder_fields.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutHeader, + EuiFormRow, + EuiCodeBlock, + EuiTitle, + EuiButton, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { usePolicyConfigContext } from '../contexts/policy_config_context'; +import { Uploader } from './uploader'; + +interface Props { + onChange: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void; + script: string; + fileName?: string; +} + +export function ScriptRecorderFields({ onChange, script, fileName }: Props) { + const [showScript, setShowScript] = useState(false); + const { isEditable } = usePolicyConfigContext(); + + const handleUpload = useCallback( + ({ scriptText, fileName: fileNameT }: { scriptText: string; fileName: string }) => { + onChange({ scriptText, fileName: fileNameT }); + }, + [onChange] + ); + + return ( + <> + + + + + + {isEditable && script ? ( + + + {fileName} + + + ) : ( + + )} + {script && ( + <> + + + + setShowScript(true)} + iconType="editorCodeBlock" + iconSide="right" + > + + + + + {isEditable && ( + onChange({ scriptText: '', fileName: '' })} + iconType="trash" + iconSide="right" + color="danger" + > + + + )} + + + + )} + {showScript && ( + setShowScript(false)} + aria-labelledby="syntheticsBrowserScriptBlockHeader" + closeButtonAriaLabel={CLOSE_BUTTON_LABEL} + > + + + + {fileName || PLACEHOLDER_FILE_NAME} + + + +
+ + {script} + +
+
+ )} + + ); +} + +const PLACEHOLDER_FILE_NAME = i18n.translate( + 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.mockFileName', + { + defaultMessage: 'test_script.js', + } +); + +const CLOSE_BUTTON_LABEL = i18n.translate( + 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.scriptRecorder.closeButtonLabel', + { + defaultMessage: 'Close script flyout', + } +); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx index 775778296fba8..50ad14aa98287 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx @@ -24,7 +24,17 @@ export const BrowserSimpleFields = memo(({ validate }) => { setFields((prevFields) => ({ ...prevFields, [configKey]: value })); }; const onChangeSourceField = useCallback( - ({ zipUrl, folder, username, password, inlineScript, params, proxyUrl }) => { + ({ + zipUrl, + folder, + username, + password, + inlineScript, + params, + proxyUrl, + isGeneratedScript, + fileName, + }) => { setFields((prevFields) => ({ ...prevFields, [ConfigKeys.SOURCE_ZIP_URL]: zipUrl, @@ -34,6 +44,13 @@ export const BrowserSimpleFields = memo(({ validate }) => { [ConfigKeys.SOURCE_ZIP_PASSWORD]: password, [ConfigKeys.SOURCE_INLINE]: inlineScript, [ConfigKeys.PARAMS]: params, + [ConfigKeys.METADATA]: { + ...prevFields[ConfigKeys.METADATA], + script_source: { + is_generated_script: isGeneratedScript, + file_name: fileName, + }, + }, })); }, [setFields] @@ -87,6 +104,9 @@ export const BrowserSimpleFields = memo(({ validate }) => { password: defaultValues[ConfigKeys.SOURCE_ZIP_PASSWORD], inlineScript: defaultValues[ConfigKeys.SOURCE_INLINE], params: defaultValues[ConfigKeys.PARAMS], + isGeneratedScript: + defaultValues[ConfigKeys.METADATA].script_source?.is_generated_script, + fileName: defaultValues[ConfigKeys.METADATA].script_source?.file_name, }), [defaultValues] )} diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx index 6f2a7c99ad0d5..e4b03e53432dd 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.tsx @@ -5,23 +5,28 @@ * 2.0. */ import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; - import { EuiTabbedContent, EuiFormRow, EuiFieldText, EuiFieldPassword, EuiSpacer, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { OptionalLabel } from '../optional_label'; import { CodeEditor } from '../code_editor'; +import { ScriptRecorderFields } from './script_recorder_fields'; import { ZipUrlTLSFields } from './zip_url_tls_fields'; import { MonacoEditorLangId } from '../types'; enum SourceType { INLINE = 'syntheticsBrowserInlineConfig', + SCRIPT_RECORDER = 'syntheticsBrowserScriptRecorderConfig', ZIP = 'syntheticsBrowserZipURLConfig', } @@ -33,6 +38,8 @@ interface SourceConfig { password: string; inlineScript: string; params: string; + isGeneratedScript?: boolean; + fileName?: string; } interface Props { @@ -48,12 +55,22 @@ export const defaultValues = { password: '', inlineScript: '', params: '', + isGeneratedScript: false, + fileName: '', +}; + +const getDefaultTab = (defaultConfig: SourceConfig) => { + if (defaultConfig.inlineScript && defaultConfig.isGeneratedScript) { + return SourceType.SCRIPT_RECORDER; + } else if (defaultConfig.inlineScript) { + return SourceType.INLINE; + } + + return SourceType.ZIP; }; export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) => { - const [sourceType, setSourceType] = useState( - defaultConfig.inlineScript ? SourceType.INLINE : SourceType.ZIP - ); + const [sourceType, setSourceType] = useState(getDefaultTab(defaultConfig)); const [config, setConfig] = useState(defaultConfig); useEffect(() => { @@ -264,6 +281,52 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) ), }, + { + id: 'syntheticsBrowserScriptRecorderConfig', + name: ( + + + + + + + + + ), + 'data-test-subj': 'syntheticsSourceTab__scriptRecorder', + content: ( + + setConfig((prevConfig) => ({ + ...prevConfig, + inlineScript: scriptText, + isGeneratedScript: true, + fileName, + })) + } + script={config.inlineScript} + fileName={config.fileName} + /> + ), + }, ]; return ( @@ -272,11 +335,17 @@ export const SourceField = ({ onChange, defaultConfig = defaultValues }: Props) initialSelectedTab={tabs.find((tab) => tab.id === sourceType)} autoFocus="selected" onTabClick={(tab) => { - setSourceType(tab.id as SourceType); if (tab.id !== sourceType) { setConfig(defaultValues); } + setSourceType(tab.id as SourceType); }} /> ); }; + +const StyledBetaBadgeWrapper = styled(EuiFlexItem)` + .euiToolTipAnchor { + display: flex; + } +`; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/uploader.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/uploader.tsx new file mode 100644 index 0000000000000..f17f11ebdccb6 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/uploader.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useRef } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiFormRow, EuiFilePicker } from '@elastic/eui'; + +interface Props { + onUpload: ({ scriptText, fileName }: { scriptText: string; fileName: string }) => void; +} + +export function Uploader({ onUpload }: Props) { + const fileReader = useRef(null); + const [error, setError] = useState(null); + const filePickerRef = useRef(null); + + const handleFileRead = (fileName: string) => { + const content = fileReader?.current?.result as string; + + if (content?.trim().slice(0, 4) !== 'step') { + setError(PARSING_ERROR); + filePickerRef.current?.removeFiles(); + return; + } + + onUpload({ scriptText: content, fileName }); + setError(null); + }; + + const handleFileChosen = (files: FileList | null) => { + if (!files || !files.length) { + onUpload({ scriptText: '', fileName: '' }); + return; + } + if (files.length && files[0].type !== 'text/javascript') { + setError(INVALID_FILE_ERROR); + filePickerRef.current?.removeFiles(); + return; + } + fileReader.current = new FileReader(); + fileReader.current.onloadend = () => handleFileRead(files[0].name); + fileReader.current.readAsText(files[0]); + }; + + return ( + + + + ); +} + +const TESTING_SCRIPT_LABEL = i18n.translate( + 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.fieldLabel', + { + defaultMessage: 'Testing script', + } +); + +const PROMPT_TEXT = i18n.translate( + 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.label', + { + defaultMessage: 'Select recorder-generated .js file', + } +); + +const INVALID_FILE_ERROR = i18n.translate( + 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.invalidFileError', + { + defaultMessage: + 'Invalid file type. Please upload a .js file generated by the Elastic Synthetics Recorder.', + } +); + +const PARSING_ERROR = i18n.translate( + 'xpack.uptime.createPackagePolicy.stepConfigure.monitorIntegrationSettingsSection.browser.uploader.parsingError', + { + defaultMessage: + 'Error uploading file. Please upload a .js file generated by the Elastic Synthetics Recorder in inline script format.', + } +); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx index bd5d2f3e5d4a2..ed1ad9a8ce65c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/zip_url_tls_fields.tsx @@ -12,7 +12,11 @@ import { EuiSwitch, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { TLSOptions, TLSConfig } from '../common/tls_options'; -import { useBrowserSimpleFieldsContext, usePolicyConfigContext } from '../contexts'; +import { + useBrowserSimpleFieldsContext, + usePolicyConfigContext, + defaultTLSFields, +} from '../contexts'; import { ConfigKeys } from '../types'; @@ -67,12 +71,23 @@ export const ZipUrlTLSFields = () => { {isZipUrlTLSEnabled ? ( >; @@ -24,6 +23,10 @@ interface IBrowserSimpleFieldsContextProvider { export const initialValues: IBrowserSimpleFields = { ...commonDefaultValues, [ConfigKeys.METADATA]: { + script_source: { + is_generated_script: false, + file_name: '', + }, is_zip_url_tls_enabled: false, }, [ConfigKeys.MONITOR_TYPE]: DataStream.BROWSER, @@ -34,13 +37,12 @@ export const initialValues: IBrowserSimpleFields = { [ConfigKeys.SOURCE_ZIP_PROXY_URL]: '', [ConfigKeys.SOURCE_INLINE]: '', [ConfigKeys.PARAMS]: '', - [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: - tlsDefaultValues[ConfigKeys.TLS_CERTIFICATE_AUTHORITIES], - [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: tlsDefaultValues[ConfigKeys.TLS_CERTIFICATE], - [ConfigKeys.ZIP_URL_TLS_KEY]: tlsDefaultValues[ConfigKeys.TLS_KEY], - [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: tlsDefaultValues[ConfigKeys.TLS_KEY_PASSPHRASE], - [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: tlsDefaultValues[ConfigKeys.TLS_VERIFICATION_MODE], - [ConfigKeys.ZIP_URL_TLS_VERSION]: tlsDefaultValues[ConfigKeys.TLS_VERSION], + [ConfigKeys.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: undefined, + [ConfigKeys.ZIP_URL_TLS_CERTIFICATE]: undefined, + [ConfigKeys.ZIP_URL_TLS_KEY]: undefined, + [ConfigKeys.ZIP_URL_TLS_KEY_PASSPHRASE]: undefined, + [ConfigKeys.ZIP_URL_TLS_VERIFICATION_MODE]: undefined, + [ConfigKeys.ZIP_URL_TLS_VERSION]: undefined, }; const defaultContext: IBrowserSimpleFieldsContext = { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts index 3d08dec46a4be..df2e9cfa6d4ea 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts @@ -7,7 +7,7 @@ export { PolicyConfigContext, PolicyConfigContextProvider, - initialValue as defaultMonitorType, + initialValue as defaultPolicyConfig, usePolicyConfigContext, } from './policy_config_context'; export { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx index 535a415c9a43d..69c0e1d7ba4fe 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/policy_config_context.tsx @@ -18,6 +18,7 @@ interface IPolicyConfigContext { isZipUrlTLSEnabled?: boolean; defaultIsTLSEnabled?: boolean; defaultIsZipUrlTLSEnabled?: boolean; + isEditable?: boolean; } interface IPolicyConfigContextProvider { @@ -25,6 +26,7 @@ interface IPolicyConfigContextProvider { defaultMonitorType?: DataStream; defaultIsTLSEnabled?: boolean; defaultIsZipUrlTLSEnabled?: boolean; + isEditable?: boolean; } export const initialValue = DataStream.HTTP; @@ -45,6 +47,7 @@ const defaultContext: IPolicyConfigContext = { defaultMonitorType: initialValue, // immutable, defaultIsTLSEnabled: false, defaultIsZipUrlTLSEnabled: false, + isEditable: false, }; export const PolicyConfigContext = createContext(defaultContext); @@ -54,6 +57,7 @@ export const PolicyConfigContextProvider = ({ defaultMonitorType = initialValue, defaultIsTLSEnabled = false, defaultIsZipUrlTLSEnabled = false, + isEditable = false, }: IPolicyConfigContextProvider) => { const [monitorType, setMonitorType] = useState(defaultMonitorType); const [isTLSEnabled, setIsTLSEnabled] = useState(defaultIsTLSEnabled); @@ -70,6 +74,7 @@ export const PolicyConfigContextProvider = ({ setIsZipUrlTLSEnabled, defaultIsTLSEnabled, defaultIsZipUrlTLSEnabled, + isEditable, }; }, [ monitorType, @@ -78,6 +83,7 @@ export const PolicyConfigContextProvider = ({ isZipUrlTLSEnabled, defaultIsTLSEnabled, defaultIsZipUrlTLSEnabled, + isEditable, ]); return ; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index 62c6f5598adb4..03bba0f8d2e54 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -54,21 +54,17 @@ const defaultTCPConfig = defaultConfig[DataStream.TCP]; describe.skip('', () => { const WrappedComponent = ({ validate = defaultValidation, - typeEditable = false, + isEditable = false, dataStreams = [DataStream.HTTP, DataStream.TCP, DataStream.ICMP, DataStream.BROWSER], }) => { return ( - + - + @@ -80,7 +76,7 @@ describe.skip('', () => { it('renders CustomFields', async () => { const { getByText, getByLabelText, queryByLabelText } = render(); - const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement; + const monitorType = getByLabelText('Monitor Type') as HTMLInputElement; const url = getByLabelText('URL') as HTMLInputElement; const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement; const monitorIntervalNumber = getByLabelText('Number') as HTMLInputElement; @@ -88,7 +84,7 @@ describe.skip('', () => { const apmServiceName = getByLabelText('APM service name') as HTMLInputElement; const maxRedirects = getByLabelText('Max redirects') as HTMLInputElement; const timeout = getByLabelText('Timeout in seconds') as HTMLInputElement; - expect(monitorType).not.toBeInTheDocument(); + expect(monitorType).toBeInTheDocument(); expect(url).toBeInTheDocument(); expect(url.value).toEqual(defaultHTTPConfig[ConfigKeys.URLS]); expect(proxyUrl).toBeInTheDocument(); @@ -117,6 +113,13 @@ describe.skip('', () => { }); }); + it('does not show monitor type dropdown when isEditable is true', async () => { + const { queryByLabelText } = render(); + const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement; + + expect(monitorType).not.toBeInTheDocument(); + }); + it('shows SSL fields when Enable SSL Fields is checked', async () => { const { findByLabelText, queryByLabelText } = render(); const enableSSL = queryByLabelText('Enable TLS configuration') as HTMLInputElement; @@ -180,7 +183,7 @@ describe.skip('', () => { it('handles switching monitor type', () => { const { getByText, getByLabelText, queryByLabelText, getAllByLabelText } = render( - + ); const monitorType = getByLabelText('Monitor Type') as HTMLInputElement; expect(monitorType).toBeInTheDocument(); @@ -244,7 +247,7 @@ describe.skip('', () => { }); it('shows resolve hostnames locally field when proxy url is filled for tcp monitors', () => { - const { getByLabelText, queryByLabelText } = render(); + const { getByLabelText, queryByLabelText } = render(); const monitorType = getByLabelText('Monitor Type') as HTMLInputElement; fireEvent.change(monitorType, { target: { value: DataStream.TCP } }); @@ -302,10 +305,7 @@ describe.skip('', () => { it('does not show monitor options that are not contained in datastreams', async () => { const { getByText, queryByText, queryByLabelText } = render( - + ); const monitorType = queryByLabelText('Monitor Type') as HTMLInputElement; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 7323464f3e9dd..98eac21a42076 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -30,13 +30,13 @@ import { BrowserSimpleFields } from './browser/simple_fields'; import { BrowserAdvancedFields } from './browser/advanced_fields'; interface Props { - typeEditable?: boolean; validate: Validation; dataStreams?: DataStream[]; } -export const CustomFields = memo(({ typeEditable = false, validate, dataStreams = [] }) => { - const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled } = usePolicyConfigContext(); +export const CustomFields = memo(({ validate, dataStreams = [] }) => { + const { monitorType, setMonitorType, isTLSEnabled, setIsTLSEnabled, isEditable } = + usePolicyConfigContext(); const isHTTP = monitorType === DataStream.HTTP; const isTCP = monitorType === DataStream.TCP; @@ -88,7 +88,7 @@ export const CustomFields = memo(({ typeEditable = false, validate, dataS > - {typeEditable && ( + {!isEditable && ( { + const { isTLSEnabled, isZipUrlTLSEnabled } = usePolicyConfigContext(); + const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext(); + const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext(); + const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext(); + const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext(); + const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext(); + const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext(); + const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext(); + const { fields: tlsFields } = useTLSFieldsContext(); + + const metadata = useMemo( + () => ({ + is_tls_enabled: isTLSEnabled, + is_zip_url_tls_enabled: isZipUrlTLSEnabled, + }), + [isTLSEnabled, isZipUrlTLSEnabled] + ); + + const policyConfig: PolicyConfig = useMemo( + () => ({ + [DataStream.HTTP]: { + ...httpSimpleFields, + ...httpAdvancedFields, + ...tlsFields, + [ConfigKeys.METADATA]: { + ...httpSimpleFields[ConfigKeys.METADATA], + ...metadata, + }, + [ConfigKeys.NAME]: name, + } as HTTPFields, + [DataStream.TCP]: { + ...tcpSimpleFields, + ...tcpAdvancedFields, + ...tlsFields, + [ConfigKeys.METADATA]: { + ...tcpSimpleFields[ConfigKeys.METADATA], + ...metadata, + }, + [ConfigKeys.NAME]: name, + } as TCPFields, + [DataStream.ICMP]: { + ...icmpSimpleFields, + [ConfigKeys.NAME]: name, + } as ICMPFields, + [DataStream.BROWSER]: { + ...browserSimpleFields, + ...browserAdvancedFields, + [ConfigKeys.METADATA]: { + ...browserSimpleFields[ConfigKeys.METADATA], + ...metadata, + }, + [ConfigKeys.NAME]: name, + } as BrowserFields, + }), + [ + metadata, + httpSimpleFields, + httpAdvancedFields, + tcpSimpleFields, + tcpAdvancedFields, + icmpSimpleFields, + browserSimpleFields, + browserAdvancedFields, + tlsFields, + name, + ] + ); + + return policyConfig; +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.test.tsx similarity index 99% rename from x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx rename to x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.test.tsx index 3bffab33adb1e..dc46a4b57bcd8 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.test.tsx @@ -4,11 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { useUpdatePolicy } from './use_update_policy'; import { renderHook } from '@testing-library/react-hooks'; -import { NewPackagePolicy } from '../../../../fleet/public'; -import { validate } from './validation'; +import { useUpdatePolicy } from './use_update_policy'; +import { NewPackagePolicy } from '../../../../../fleet/public'; +import { validate } from '../validation'; import { ConfigKeys, DataStream, @@ -20,8 +19,8 @@ import { ITLSFields, HTTPFields, BrowserFields, -} from './types'; -import { defaultConfig } from './synthetics_policy_create_extension'; +} from '../types'; +import { defaultConfig } from '../synthetics_policy_create_extension'; describe('useBarChartsHooks', () => { const newPolicy: NewPackagePolicy = { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.ts similarity index 95% rename from x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts rename to x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.ts index 145a86c6bd50d..17ded6385da4f 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/hooks/use_update_policy.ts @@ -5,9 +5,9 @@ * 2.0. */ import { useEffect, useRef, useState } from 'react'; -import { NewPackagePolicy } from '../../../../fleet/public'; -import { ConfigKeys, DataStream, Validation, ICustomFields } from './types'; -import { formatters } from './helpers/formatters'; +import { NewPackagePolicy } from '../../../../../fleet/public'; +import { ConfigKeys, DataStream, Validation, ICustomFields } from '../types'; +import { formatters } from '../helpers/formatters'; interface Props { monitorType: DataStream; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx index 3db7ac424e651..4fa101a329cd0 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx @@ -8,36 +8,21 @@ import React, { memo, useEffect, useMemo } from 'react'; import { PackagePolicyCreateExtensionComponentProps } from '../../../../fleet/public'; import { useTrackPageview } from '../../../../observability/public'; -import { - PolicyConfig, - DataStream, - ConfigKeys, - HTTPFields, - TCPFields, - ICMPFields, - BrowserFields, -} from './types'; +import { PolicyConfig, DataStream } from './types'; import { usePolicyConfigContext, - useTCPSimpleFieldsContext, - useTCPAdvancedFieldsContext, - useICMPSimpleFieldsContext, - useHTTPSimpleFieldsContext, - useHTTPAdvancedFieldsContext, - useTLSFieldsContext, - useBrowserSimpleFieldsContext, - useBrowserAdvancedFieldsContext, - defaultHTTPAdvancedFields, defaultHTTPSimpleFields, - defaultICMPSimpleFields, + defaultHTTPAdvancedFields, defaultTCPSimpleFields, defaultTCPAdvancedFields, + defaultICMPSimpleFields, defaultBrowserSimpleFields, defaultBrowserAdvancedFields, defaultTLSFields, } from './contexts'; import { CustomFields } from './custom_fields'; -import { useUpdatePolicy } from './use_update_policy'; +import { useUpdatePolicy } from './hooks/use_update_policy'; +import { usePolicy } from './hooks/use_policy'; import { validate } from './validation'; export const defaultConfig: PolicyConfig = { @@ -65,55 +50,12 @@ export const defaultConfig: PolicyConfig = { */ export const SyntheticsPolicyCreateExtension = memo( ({ newPolicy, onChange }) => { - const { monitorType, isTLSEnabled, isZipUrlTLSEnabled } = usePolicyConfigContext(); - const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext(); - const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext(); - const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext(); - const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext(); - const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext(); - const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext(); - const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext(); - const { fields: tlsFields } = useTLSFieldsContext(); - - const metaData = useMemo( - () => ({ - is_tls_enabled: isTLSEnabled, - is_zip_url_tls_enabled: isZipUrlTLSEnabled, - }), - [isTLSEnabled, isZipUrlTLSEnabled] - ); - - const policyConfig: PolicyConfig = { - [DataStream.HTTP]: { - ...httpSimpleFields, - ...httpAdvancedFields, - ...tlsFields, - [ConfigKeys.METADATA]: metaData, - [ConfigKeys.NAME]: newPolicy.name, - } as HTTPFields, - [DataStream.TCP]: { - ...tcpSimpleFields, - ...tcpAdvancedFields, - ...tlsFields, - [ConfigKeys.METADATA]: metaData, - [ConfigKeys.NAME]: newPolicy.name, - } as TCPFields, - [DataStream.ICMP]: { - ...icmpSimpleFields, - [ConfigKeys.NAME]: newPolicy.name, - } as ICMPFields, - [DataStream.BROWSER]: { - ...browserSimpleFields, - ...browserAdvancedFields, - ...tlsFields, - [ConfigKeys.METADATA]: metaData, - [ConfigKeys.NAME]: newPolicy.name, - } as BrowserFields, - }; - useTrackPageview({ app: 'fleet', path: 'syntheticsCreate' }); useTrackPageview({ app: 'fleet', path: 'syntheticsCreate', delay: 15000 }); + const { monitorType } = usePolicyConfigContext(); + const policyConfig: PolicyConfig = usePolicy(newPolicy.name); + const dataStreams: DataStream[] = useMemo(() => { return newPolicy.inputs.map((input) => { return input.type.replace(/synthetics\//g, '') as DataStream; @@ -143,7 +85,7 @@ export const SyntheticsPolicyCreateExtension = memo; + return ; } ); SyntheticsPolicyCreateExtension.displayName = 'SyntheticsPolicyCreateExtension'; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx index 8e441d4eed6e3..1e01f43439a31 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx @@ -5,32 +5,14 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { memo } from 'react'; import { PackagePolicyEditExtensionComponentProps } from '../../../../fleet/public'; import { useTrackPageview } from '../../../../observability/public'; -import { - usePolicyConfigContext, - useTCPSimpleFieldsContext, - useTCPAdvancedFieldsContext, - useICMPSimpleFieldsContext, - useHTTPSimpleFieldsContext, - useHTTPAdvancedFieldsContext, - useTLSFieldsContext, - useBrowserSimpleFieldsContext, - useBrowserAdvancedFieldsContext, -} from './contexts'; -import { - ICustomFields, - DataStream, - HTTPFields, - TCPFields, - ICMPFields, - BrowserFields, - ConfigKeys, - PolicyConfig, -} from './types'; +import { usePolicyConfigContext } from './contexts'; +import { ICustomFields, PolicyConfig } from './types'; import { CustomFields } from './custom_fields'; -import { useUpdatePolicy } from './use_update_policy'; +import { useUpdatePolicy } from './hooks/use_update_policy'; +import { usePolicy } from './hooks/use_policy'; import { validate } from './validation'; interface SyntheticsPolicyEditExtensionProps { @@ -47,50 +29,9 @@ export const SyntheticsPolicyEditExtension = memo { useTrackPageview({ app: 'fleet', path: 'syntheticsEdit' }); useTrackPageview({ app: 'fleet', path: 'syntheticsEdit', delay: 15000 }); - const { monitorType, isTLSEnabled, isZipUrlTLSEnabled } = usePolicyConfigContext(); - const { fields: httpSimpleFields } = useHTTPSimpleFieldsContext(); - const { fields: tcpSimpleFields } = useTCPSimpleFieldsContext(); - const { fields: icmpSimpleFields } = useICMPSimpleFieldsContext(); - const { fields: httpAdvancedFields } = useHTTPAdvancedFieldsContext(); - const { fields: tcpAdvancedFields } = useTCPAdvancedFieldsContext(); - const { fields: tlsFields } = useTLSFieldsContext(); - const { fields: browserSimpleFields } = useBrowserSimpleFieldsContext(); - const { fields: browserAdvancedFields } = useBrowserAdvancedFieldsContext(); - const metadata = useMemo( - () => ({ - is_tls_enabled: isTLSEnabled, - is_zip_url_tls_enabled: isZipUrlTLSEnabled, - }), - [isTLSEnabled, isZipUrlTLSEnabled] - ); - - const policyConfig: PolicyConfig = { - [DataStream.HTTP]: { - ...httpSimpleFields, - ...httpAdvancedFields, - ...tlsFields, - [ConfigKeys.METADATA]: metadata, - [ConfigKeys.NAME]: newPolicy.name, - } as HTTPFields, - [DataStream.TCP]: { - ...tcpSimpleFields, - ...tcpAdvancedFields, - ...tlsFields, - [ConfigKeys.METADATA]: metadata, - [ConfigKeys.NAME]: newPolicy.name, - } as TCPFields, - [DataStream.ICMP]: { - ...icmpSimpleFields, - [ConfigKeys.NAME]: newPolicy.name, - } as ICMPFields, - [DataStream.BROWSER]: { - ...browserSimpleFields, - ...browserAdvancedFields, - [ConfigKeys.METADATA]: metadata, - [ConfigKeys.NAME]: newPolicy.name, - } as BrowserFields, - }; + const { monitorType } = usePolicyConfigContext(); + const policyConfig: PolicyConfig = usePolicy(newPolicy.name); useUpdatePolicy({ defaultConfig, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx index e874ca73d951b..c141e33ea1595 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx @@ -1075,6 +1075,59 @@ describe('', () => { expect(queryByLabelText('Host')).not.toBeInTheDocument(); }); + it.each([ + [true, 'Testing script'], + [false, 'Inline script'], + ])( + 'browser monitors - auto selects the right tab depending on source metadata', + async (isGeneratedScript, text) => { + const currentPolicy = { + ...defaultCurrentPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[1], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[2], + enabled: false, + }, + { + ...defaultNewPolicy.inputs[3], + enabled: true, + streams: [ + { + ...defaultNewPolicy.inputs[3].streams[0], + vars: { + ...defaultNewPolicy.inputs[3].streams[0].vars, + 'source.inline.script': { + type: 'yaml', + value: JSON.stringify('step(() => {})'), + }, + __ui: { + type: 'yaml', + value: JSON.stringify({ + script_source: { + is_generated_script: isGeneratedScript, + }, + }), + }, + }, + }, + ], + }, + ], + }; + + const { getByText } = render(); + + expect(getByText(text)).toBeInTheDocument(); + } + ); it('hides tls fields when metadata.is_tls_enabled is false', async () => { const { getByLabelText, queryByLabelText } = render( diff --git a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx index f391c6c271f69..13296ee4521cd 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx @@ -129,6 +129,10 @@ export enum ConfigKeys { export interface Metadata { is_tls_enabled?: boolean; is_zip_url_tls_enabled?: boolean; + script_source?: { + is_generated_script: boolean; + file_name: string; + }; } export interface ICommonFields { diff --git a/x-pack/plugins/uptime/scripts/e2e.js b/x-pack/plugins/uptime/scripts/e2e.js index e2a8dfaf25c93..e7c0cb612646d 100644 --- a/x-pack/plugins/uptime/scripts/e2e.js +++ b/x-pack/plugins/uptime/scripts/e2e.js @@ -28,9 +28,14 @@ const { argv } = yargs(process.argv.slice(2)) type: 'boolean', description: 'Opens the Playwright Test Runner', }) + .option('kibana-install-dir', { + default: '', + type: 'string', + description: 'Path to the Kibana install directory', + }) .help(); -const { server, runner, open } = argv; +const { server, runner, open, kibanaInstallDir } = argv; const e2eDir = path.join(__dirname, '../e2e'); @@ -44,9 +49,12 @@ if (server) { const config = './playwright_run.ts'; function executeRunner() { - childProcess.execSync(`node ../../../scripts/${ftrScript} --config ${config}`, { - cwd: e2eDir, - stdio: 'inherit', - }); + childProcess.execSync( + `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`, + { + cwd: e2eDir, + stdio: 'inherit', + } + ); } executeRunner(); diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 4babe0bd6ff88..e06661b000203 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - describe('ml', () => { + // Failing: See https://github.com/elastic/kibana/issues/115666 + describe.skip('ml', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/api_integration/apis/maps/fonts_api.js b/x-pack/test/api_integration/apis/maps/fonts_api.js index afde003b05f2d..35017e6e37db8 100644 --- a/x-pack/test/api_integration/apis/maps/fonts_api.js +++ b/x-pack/test/api_integration/apis/maps/fonts_api.js @@ -6,11 +6,33 @@ */ import expect from '@kbn/expect'; +import path from 'path'; +import { copyFile, rm } from 'fs/promises'; export default function ({ getService }) { const supertest = getService('supertest'); + const log = getService('log'); describe('fonts', () => { + // [HACK]: On CI tests are run from the different directories than the built and running Kibana + // instance. To workaround that we use Kibana `process.cwd()` to construct font path manually. + // x-pack tests can be run from root directory or from within x-pack so need to cater for both possibilities. + const fontPath = path.join( + process.cwd().replace(/x-pack.*$/, ''), + 'x-pack/plugins/maps/server/fonts/open_sans/0-255.pbf' + ); + const destinationPath = path.join(path.dirname(fontPath), '..', path.basename(fontPath)); + + before(async () => { + log.debug(`Copying test file from '${fontPath}' to '${destinationPath}'`); + await copyFile(fontPath, destinationPath); + }); + + after(async () => { + log.debug(`Removing test file '${destinationPath}'`); + await rm(destinationPath); + }); + it('should return fonts', async () => { const resp = await supertest .get(`/api/maps/fonts/Open%20Sans%20Regular,Arial%20Unicode%20MS%20Regular/0-255`) @@ -25,12 +47,12 @@ export default function ({ getService }) { .expect(404); }); - it('should return 404 when file is not in font folder (../)', async () => { - await supertest.get(`/api/maps/fonts/open_sans/..%2fopen_sans%2f0-255`).expect(404); + it('should return 404 when file is not in font folder (..)', async () => { + await supertest.get(`/api/maps/fonts/open_sans/..%2f0-255`).expect(404); }); - it('should return 404 when file is not in font folder (./../)', async () => { - await supertest.get(`/api/maps/fonts/open_sans/.%2f..%2fopen_sans%2f0-255`).expect(404); + it('should return 404 when file is not in font folder (./..)', async () => { + await supertest.get(`/api/maps/fonts/open_sans/.%2f..%2f0-255`).expect(404); }); }); } diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts index 1071e9fae3417..03b1beffa7993 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/export_rules.ts @@ -76,7 +76,13 @@ export default ({ getService }: FtrProviderContext): void => { const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]); expect(bodySplitAndParsed).to.eql({ - exported_count: 1, + exported_exception_list_count: 0, + exported_exception_list_item_count: 0, + exported_rules_count: 1, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, missing_rules: [], missing_rules_count: 0, }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts index 9b7c75bab3100..3c033d2077c54 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts @@ -51,9 +51,9 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map( - (signal) => (signal._source?.host_alias as HostAlias).name - ); + const hits = signalsOpen.hits.hits + .map((signal) => (signal._source?.host_alias as HostAlias).name) + .sort(); expect(hits).to.eql(['host name 1', 'host name 2', 'host name 3', 'host name 4']); }); @@ -63,7 +63,9 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((signal) => (signal._source?.host as HostAlias).name); + const hits = signalsOpen.hits.hits + .map((signal) => (signal._source?.host as HostAlias).name) + .sort(); expect(hits).to.eql(['host name 1', 'host name 2', 'host name 3', 'host name 4']); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts new file mode 100644 index 0000000000000..58b5f98ff0c0d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts @@ -0,0 +1,107 @@ +/* + * 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 { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + waitForRuleSuccessOrStatus, + getRuleForSignalTesting, + createRuleWithAuth, + getThresholdRuleForSignalTesting, +} from '../../utils'; +import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; +import { ROLES } from '../../../../plugins/security_solution/common/test'; +import { ThresholdCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('check_privileges', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/alias'); + await createSignalsIndex(supertest); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/alias'); + await deleteSignalsIndex(supertest); + }); + + beforeEach(async () => { + await deleteAllAlerts(supertest); + }); + + afterEach(async () => { + await deleteAllAlerts(supertest); + }); + + describe('should set status to partial failure when user has no access', () => { + const indexTestCases = [ + ['host_alias'], + ['host_alias', 'auditbeat-8.0.0'], + ['host_alias*'], + ['host_alias*', 'auditbeat-*'], + ]; + indexTestCases.forEach((index) => { + it(`for KQL rule with index param: ${index}`, async () => { + const rule = getRuleForSignalTesting(index); + await createUserAndRole(getService, ROLES.detections_admin); + const { id } = await createRuleWithAuth(supertestWithoutAuth, rule, { + user: ROLES.detections_admin, + pass: 'changeme', + }); + await waitForRuleSuccessOrStatus(supertest, id, 'partial failure'); + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [id] }) + .expect(200); + expect(body[id]?.current_status?.last_success_message).to.eql( + `This rule may not have the required read privileges to the following indices/index patterns: ["${index[0]}"]` + ); + + await deleteUserAndRole(getService, ROLES.detections_admin); + }); + + it(`for threshold rule with index param: ${index}`, async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(index), + threshold: { + field: [], + value: 700, + }, + }; + await createUserAndRole(getService, ROLES.detections_admin); + const { id } = await createRuleWithAuth(supertestWithoutAuth, rule, { + user: ROLES.detections_admin, + pass: 'changeme', + }); + await waitForRuleSuccessOrStatus(supertest, id, 'partial failure'); + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [id] }) + .expect(200); + expect(body[id]?.current_status?.last_success_message).to.eql( + `This rule may not have the required read privileges to the following indices/index patterns: ["${index[0]}"]` + ); + + await deleteUserAndRole(getService, ROLES.detections_admin); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts index b0f208aadaf1b..6d04ffc67c573 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; +import type SuperTest from 'supertest'; import { createListsIndex, deleteAllExceptions, @@ -25,6 +26,45 @@ import { waitForSignalsToBePresent, } from '../../utils'; +interface Host { + os: { + type?: string; + name?: string; + }; +} + +/** + * Convenience method to get signals by host and sort them for better deterministic testing + * since Elastic can return the hits back in any order we want to sort them on return for testing. + * @param supertest Super test for testing. + * @param id The signals id + * @returns The array of hosts sorted + */ +export const getHostHits = async ( + supertest: SuperTest.SuperTest, + id: string +): Promise => { + const signalsOpen = await getSignalsById(supertest, id); + return signalsOpen.hits.hits + .map((hit) => hit._source?.host as Host) + .sort((a, b) => { + let sortOrder = 0; + if (a.os.name != null && b.os.name != null) { + sortOrder += a.os.name.localeCompare(b.os.name); + } + if (a.os.type != null && b.os.type != null) { + sortOrder += a.os.type.localeCompare(b.os.type); + } + if (a.os.type != null && b.os.name != null) { + sortOrder += a.os.type.localeCompare(b.os.name); + } + if (a.os.name != null && b.os.type != null) { + sortOrder += a.os.name.localeCompare(b.os.type); + } + return sortOrder; + }); +}; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); @@ -64,20 +104,19 @@ export default ({ getService }: FtrProviderContext) => { const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host).sort(); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { os: { type: 'linux' }, }, { - os: { type: 'windows' }, + os: { type: 'linux' }, }, { os: { type: 'macos' }, }, { - os: { type: 'linux' }, + os: { type: 'windows' }, }, ]); }); @@ -87,20 +126,19 @@ export default ({ getService }: FtrProviderContext) => { const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host).sort(); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { os: { name: 'Linux' }, }, { - os: { name: 'Windows' }, + os: { name: 'Linux' }, }, { os: { name: 'Macos' }, }, { - os: { name: 'Linux' }, + os: { name: 'Windows' }, }, ]); }); @@ -130,17 +168,16 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { name: 'Windows' }, + os: { name: 'Linux' }, }, { os: { name: 'Macos' }, }, { - os: { name: 'Linux' }, + os: { name: 'Windows' }, }, ]); }); @@ -167,17 +204,16 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { name: 'Windows' }, + os: { name: 'Linux' }, }, { os: { name: 'Macos' }, }, { - os: { name: 'Linux' }, + os: { name: 'Windows' }, }, ]); }); @@ -215,14 +251,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { name: 'Macos' }, + os: { name: 'Linux' }, }, { - os: { name: 'Linux' }, + os: { name: 'Macos' }, }, ]); }); @@ -260,14 +295,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { name: 'Macos' }, + os: { name: 'Linux' }, }, { - os: { name: 'Linux' }, + os: { name: 'Macos' }, }, ]); }); @@ -296,17 +330,16 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'windows' }, + os: { type: 'linux' }, }, { os: { type: 'macos' }, }, { - os: { type: 'linux' }, + os: { type: 'windows' }, }, ]); }); @@ -333,17 +366,16 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'windows' }, + os: { type: 'linux' }, }, { os: { type: 'macos' }, }, { - os: { type: 'linux' }, + os: { type: 'windows' }, }, ]); }); @@ -381,14 +413,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'macos' }, + os: { type: 'linux' }, }, { - os: { type: 'linux' }, + os: { type: 'macos' }, }, ]); }); @@ -426,14 +457,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'macos' }, + os: { type: 'linux' }, }, { - os: { type: 'linux' }, + os: { type: 'macos' }, }, ]); }); @@ -462,14 +492,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 6, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'windows' }, + os: { type: 'linux' }, }, { - os: { name: 'Windows' }, + os: { name: 'Linux' }, }, { os: { type: 'macos' }, @@ -478,10 +507,10 @@ export default ({ getService }: FtrProviderContext) => { os: { name: 'Macos' }, }, { - os: { type: 'linux' }, + os: { type: 'windows' }, }, { - os: { name: 'Linux' }, + os: { name: 'Windows' }, }, ]); }); @@ -508,14 +537,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 6, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'windows' }, + os: { type: 'linux' }, }, { - os: { name: 'Windows' }, + os: { name: 'Linux' }, }, { os: { type: 'macos' }, @@ -524,10 +552,10 @@ export default ({ getService }: FtrProviderContext) => { os: { name: 'Macos' }, }, { - os: { type: 'linux' }, + os: { type: 'windows' }, }, { - os: { name: 'Linux' }, + os: { name: 'Windows' }, }, ]); }); @@ -565,20 +593,19 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'macos' }, + os: { type: 'linux' }, }, { - os: { name: 'Macos' }, + os: { name: 'Linux' }, }, { - os: { type: 'linux' }, + os: { type: 'macos' }, }, { - os: { name: 'Linux' }, + os: { name: 'Macos' }, }, ]); }); @@ -616,20 +643,19 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'macos' }, + os: { type: 'linux' }, }, { - os: { name: 'Macos' }, + os: { name: 'Linux' }, }, { - os: { type: 'linux' }, + os: { type: 'macos' }, }, { - os: { name: 'Linux' }, + os: { name: 'Macos' }, }, ]); }); @@ -668,8 +694,7 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { os: { type: 'macos' }, @@ -708,8 +733,7 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { os: { type: 'macos' }, @@ -741,17 +765,16 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 3, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { os: { type: 'linux' }, }, { - os: { type: 'macos' }, + os: { type: 'linux' }, }, { - os: { type: 'linux' }, + os: { type: 'macos' }, }, ]); }); @@ -778,14 +801,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'macos' }, + os: { type: 'linux' }, }, { - os: { type: 'linux' }, + os: { type: 'macos' }, }, ]); }); @@ -812,14 +834,13 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 2, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { - os: { type: 'macos' }, + os: { type: 'linux' }, }, { - os: { type: 'linux' }, + os: { type: 'macos' }, }, ]); }); @@ -846,20 +867,19 @@ export default ({ getService }: FtrProviderContext) => { ); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 4, [id]); - const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source?.host); + const hits = await getHostHits(supertest, id); expect(hits).to.eql([ { os: { type: 'linux' }, }, { - os: { type: 'windows' }, + os: { type: 'linux' }, }, { os: { type: 'macos' }, }, { - os: { type: 'linux' }, + os: { type: 'windows' }, }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts index f7208a8832c4d..912596ed7ca00 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts @@ -499,7 +499,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.float).sort(); expect(hits).to.eql(['1.1', '1.2', '1.3']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts index 42152fd18473a..da9219e4b52f6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts @@ -501,7 +501,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.integer).sort(); expect(hits).to.eql(['2', '3', '4']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts index 147e6058dffa8..526c6d1c988ce 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts @@ -151,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); - expect(ips).to.eql([[]]); + expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); it('should filter a CIDR range of "127.0.0.1/30"', async () => { @@ -167,7 +167,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); expect(ips).to.eql([ @@ -190,7 +190,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); @@ -346,7 +346,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); - expect(ips).to.eql([[]]); + expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -392,8 +392,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/115315 - describe.skip('"exists" operator', () => { + describe('"exists" operator', () => { it('will return 1 empty result if matching against ip', async () => { const rule = getRuleForSignalTesting(['ip_as_array']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ @@ -408,7 +407,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); - expect(ips).to.eql([[]]); + expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -487,8 +486,7 @@ export default ({ getService }: FtrProviderContext) => { expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); }); - // FLAKY https://github.com/elastic/kibana/issues/89052 - it.skip('will return 1 result if we have a list that includes all ips', async () => { + it('will return 1 result if we have a list that includes all ips', async () => { await importFile( supertest, 'ip', @@ -512,7 +510,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); - expect(ips).to.eql([[]]); + expect(ips.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); it('will return 2 results if we have a list which contains the CIDR ranges of "127.0.0.1/32, 127.0.0.2/31, 127.0.0.4/30"', async () => { @@ -546,7 +544,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); @@ -577,7 +575,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const ips = signalsOpen.hits.hits.map((hit) => hit._source?.ip).sort(); expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts index e852558aaa6a8..8571aa8eeaa60 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext) => { const rule = getRuleForSignalTesting(['keyword_as_array']); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 3, [id]); + await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); expect(hits).to.eql([ @@ -84,7 +84,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 2, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); expect(hits).to.eql([ @@ -153,7 +153,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -281,7 +281,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -328,8 +328,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"exists" operator', () => { - // FLAKY https://github.com/elastic/kibana/issues/115308 - it.skip('will return 1 results if matching against keyword for the empty array', async () => { + it('will return 1 results if matching against keyword for the empty array', async () => { const rule = getRuleForSignalTesting(['keyword_as_array']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -343,7 +342,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -399,7 +398,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 3, [id]); + await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); expect(hits).to.eql([ @@ -437,7 +436,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 2, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); expect(hits).to.eql([ @@ -497,8 +496,7 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); }); - // FLAKY https://github.com/elastic/kibana/issues/115304 - it.skip('will return only the empty array for results if we have a list that includes all keyword', async () => { + it('will return only the empty array for results if we have a list that includes all keyword', async () => { await importFile( supertest, 'keyword', @@ -522,7 +520,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.keyword).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts index 35573edea3c39..8d5f1515e4ab6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts @@ -499,7 +499,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.long).sort(); expect(hits).to.eql(['2', '3', '4']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts index 4e4823fcf747f..367e68f7f9ed1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts @@ -56,8 +56,7 @@ export default ({ getService }: FtrProviderContext) => { await deleteListsIndex(supertest); }); - // FLAKY: https://github.com/elastic/kibana/issues/115310 - describe.skip('"is" operator', () => { + describe('"is" operator', () => { it('should find all the text from the data set when no exceptions are set on the rule', async () => { const rule = getRuleForSignalTesting(['text']); const { id } = await createRule(supertest, rule); @@ -241,7 +240,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql(['word four', 'word three', 'word two']); @@ -344,6 +343,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); @@ -618,7 +618,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql(['word four', 'word three', 'word two']); @@ -646,7 +646,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql(['word four', 'word three', 'word two']); @@ -669,7 +669,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql(['word four', 'word two']); @@ -850,7 +850,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 2, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql(['word one', 'word three']); @@ -878,7 +878,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts index f0a5fe7c1ffb1..3eedabd41d663 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts @@ -58,7 +58,7 @@ export default ({ getService }: FtrProviderContext) => { const rule = getRuleForSignalTesting(['text_as_array']); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 3, [id]); + await waitForSignalsToBePresent(supertest, 4, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql([ @@ -82,7 +82,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 2, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql([ @@ -151,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -279,7 +279,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -326,8 +326,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"exists" operator', () => { - // FLAKY https://github.com/elastic/kibana/issues/115313 - it.skip('will return 1 results if matching against text for the empty array', async () => { + it('will return 1 results if matching against text for the empty array', async () => { const rule = getRuleForSignalTesting(['text_as_array']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -341,7 +340,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); @@ -435,7 +434,7 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 2, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); expect(hits).to.eql([ @@ -495,8 +494,7 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); }); - // FLAKY https://github.com/elastic/kibana/issues/113418 - it.skip('will return only the empty array for results if we have a list that includes all text', async () => { + it('will return only the empty array for results if we have a list that includes all text', async () => { await importFile( supertest, 'text', @@ -520,7 +518,7 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, id); const signalsOpen = await getSignalsById(supertest, id); const hits = signalsOpen.hits.hits.map((hit) => hit._source?.text).sort(); - expect(hits).to.eql([[]]); + expect(hits.flat(Number.MAX_SAFE_INTEGER)).to.eql([]); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts index 1071e9fae3417..03b1beffa7993 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts @@ -76,7 +76,13 @@ export default ({ getService }: FtrProviderContext): void => { const bodySplitAndParsed = JSON.parse(body.toString().split(/\n/)[1]); expect(bodySplitAndParsed).to.eql({ - exported_count: 1, + exported_exception_list_count: 0, + exported_exception_list_item_count: 0, + exported_rules_count: 1, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, missing_rules: [], missing_rules_count: 0, }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 1b88c4fe21b49..00147a2ec2ef7 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -18,6 +18,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./add_actions')); loadTestFile(require.resolve('./update_actions')); loadTestFile(require.resolve('./add_prepackaged_rules')); + loadTestFile(require.resolve('./check_privileges')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); loadTestFile(require.resolve('./create_index')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts index 53613624067e1..83166619b152d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts @@ -57,7 +57,13 @@ export default ({ getService }: FtrProviderContext): void => { const exportDetails = JSON.parse(exportDetailsJson); expect(exportDetails).to.eql({ - exported_count: 1, + exported_exception_list_count: 0, + exported_exception_list_item_count: 0, + exported_rules_count: 1, + missing_exception_list_item_count: 0, + missing_exception_list_items: [], + missing_exception_lists: [], + missing_exception_lists_count: 0, missing_rules: [], missing_rules_count: 0, }); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index eeae21c3b7bad..dd1b1ba966175 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -916,6 +916,25 @@ export const createRule = async ( return body; }; +/** + * Helper to cut down on the noise in some of the tests. This checks for + * an expected 200 still and does not try to any retries. + * @param supertest The supertest deps + * @param rule The rule to create + */ +export const createRuleWithAuth = async ( + supertest: SuperTest.SuperTest, + rule: CreateRulesSchema, + auth: { user: string; pass: string } +): Promise => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .auth(auth.user, auth.pass) + .send(rule); + return body; +}; + /** * Helper to cut down on the noise in some of the tests. This checks for * an expected 200 still and does not do any retries. diff --git a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts index e244c907a76d6..a0b3c636a2f1a 100644 --- a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts +++ b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts @@ -34,8 +34,7 @@ export default function ({ getService }: FtrProviderContext) { }, }; - // FLAKY https://github.com/elastic/kibana/issues/113890 - describe.skip('creation with runtime mappings', function () { + describe('creation with runtime mappings', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await transform.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts index c4fa9f643e69e..0dc5cc8fae2d5 100644 --- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts +++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts @@ -7,12 +7,10 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MlCommonUI } from './common_ui'; import { MlDashboardJobSelectionTable } from './dashboard_job_selection_table'; export function MachineLearningDashboardEmbeddablesProvider( { getService }: FtrProviderContext, - mlCommonUI: MlCommonUI, mlDashboardJobSelectionTable: MlDashboardJobSelectionTable ) { const retry = getService('retry'); @@ -22,14 +20,14 @@ export function MachineLearningDashboardEmbeddablesProvider( return { async assertAnomalyChartsEmbeddableInitializerExists() { - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail('mlAnomalyChartsEmbeddableInitializer'); + await retry.tryForTime(10 * 1000, async () => { + await testSubjects.existOrFail('mlAnomalyChartsEmbeddableInitializer', { timeout: 1000 }); }); }, async assertAnomalyChartsEmbeddableInitializerNotExists() { - await retry.tryForTime(5000, async () => { - await testSubjects.missingOrFail('mlAnomalyChartsEmbeddableInitializer'); + await retry.tryForTime(10 * 1000, async () => { + await testSubjects.missingOrFail('mlAnomalyChartsEmbeddableInitializer', { timeout: 1000 }); }); }, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index 45ba4c5c34833..2c375d47b0b3b 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -18,10 +18,11 @@ import { } from '../../../../plugins/ml/common/util/analytics_utils'; export function MachineLearningDataFrameAnalyticsCreationProvider( - { getService }: FtrProviderContext, + { getPageObject, getService }: FtrProviderContext, mlCommonUI: MlCommonUI, mlApi: MlApi ) { + const headerPage = getPageObject('header'); const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); const retry = getService('retry'); @@ -111,10 +112,12 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async assertSourceDataPreviewExists() { + await headerPage.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('mlAnalyticsCreationDataGrid loaded', { timeout: 5000 }); }, async assertIndexPreviewHistogramChartButtonExists() { + await headerPage.waitUntilLoadingHasFinished(); await testSubjects.existOrFail('mlAnalyticsCreationDataGridHistogramButton'); }, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index ac728e6b88303..a1bf8c6a65d70 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -73,7 +73,7 @@ export function MachineLearningDataFrameAnalyticsResultsProvider( async assertTotalFeatureImportanceEvaluatePanelExists() { await testSubjects.existOrFail('mlDFExpandableSection-FeatureImportanceSummary'); - await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 5000 }); + await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 30 * 1000 }); }, async assertFeatureImportanceDecisionPathElementsExists() { @@ -167,17 +167,19 @@ export function MachineLearningDataFrameAnalyticsResultsProvider( async openFeatureImportancePopover() { this.assertResultsTableNotEmpty(); - const featureImportanceCell = await this.getFirstFeatureImportanceCell(); - await featureImportanceCell.focus(); - const interactionButton = await featureImportanceCell.findByTagName('button'); + await retry.tryForTime(30 * 1000, async () => { + const featureImportanceCell = await this.getFirstFeatureImportanceCell(); + await featureImportanceCell.focus(); + const interactionButton = await featureImportanceCell.findByTagName('button'); - // simulate hover and wait for button to appear - await featureImportanceCell.moveMouseTo(); - await this.waitForInteractionButtonToDisplay(interactionButton); + // simulate hover and wait for button to appear + await featureImportanceCell.moveMouseTo(); + await this.waitForInteractionButtonToDisplay(interactionButton); - // open popover - await interactionButton.click(); - await testSubjects.existOrFail('mlDFAFeatureImportancePopover'); + // open popover + await interactionButton.click(); + await testSubjects.existOrFail('mlDFAFeatureImportancePopover', { timeout: 1000 }); + }); }, async getFirstFeatureImportanceCell(): Promise { diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index 5850919f2adc3..0bfb37c6c94f8 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -196,6 +196,11 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F analyticsId: string, shouldBeDisplayed: boolean ) { + await this.waitForRefreshButtonLoaded(); + await testSubjects.click('~mlAnalyticsRefreshListButton'); + await this.waitForRefreshButtonLoaded(); + await testSubjects.existOrFail('mlAnalyticsJobList', { timeout: 30 * 1000 }); + if (shouldBeDisplayed) { await this.filterWithSearchString(analyticsId, 1); } else { diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 7f32968ec4326..6883946452629 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -33,8 +33,11 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ }, async clickUseFullDataButton(expectedFormattedTotalDocCount: string) { - await testSubjects.clickWhenNotDisabled('dataVisualizerButtonUseFullData'); - await this.assertTotalDocumentCount(expectedFormattedTotalDocCount); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.clickWhenNotDisabled('dataVisualizerButtonUseFullData'); + await testSubjects.clickWhenNotDisabled('superDatePickerApplyTimeButton'); + await this.assertTotalDocumentCount(expectedFormattedTotalDocCount); + }); }, async assertTotalDocCountHeaderExist() { diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index d50ec371d7c23..17302b2782223 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -67,7 +67,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { const dashboardJobSelectionTable = MachineLearningDashboardJobSelectionTableProvider(context); const dashboardEmbeddables = MachineLearningDashboardEmbeddablesProvider( context, - commonUI, dashboardJobSelectionTable ); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts index d35d34fde5bcc..c21026d5df3d2 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_exception_list.ts @@ -77,7 +77,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(400); expect(exportBody).to.eql({ - message: 'exception list with list_id: not_exist does not exist', + message: 'exception list with list_id: not_exist or id: not_exist does not exist', status_code: 400, }); });