diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index 60b702fa1d8fc..6958ec4530ae8 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -39,6 +39,9 @@ disabled: - x-pack/test/fleet_cypress/config.space_awareness.ts - x-pack/test/fleet_cypress/visual_config.ts + # Default http2 config to use for performance journeys + - x-pack/performance/configs/http2_config.ts + defaultQueue: 'n2-4-spot' enabled: - test/accessibility/config.ts @@ -345,6 +348,7 @@ enabled: - x-pack/test/usage_collection/config.ts - x-pack/performance/journeys_e2e/aiops_log_rate_analysis.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard.ts + - x-pack/performance/journeys_e2e/ecommerce_dashboard_http2.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard_map_only.ts - x-pack/performance/journeys_e2e/flight_dashboard.ts - x-pack/performance/journeys_e2e/login.ts @@ -358,6 +362,7 @@ enabled: - x-pack/performance/journeys_e2e/web_logs_dashboard_esql.ts - x-pack/performance/journeys_e2e/web_logs_dashboard_dataview.ts - x-pack/performance/journeys_e2e/data_stress_test_lens.ts + - x-pack/performance/journeys_e2e/data_stress_test_lens_http2.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard_saved_search_only.ts - x-pack/performance/journeys_e2e/ecommerce_dashboard_tsvb_gauge_only.ts - x-pack/performance/journeys_e2e/dashboard_listing_page.ts diff --git a/.buildkite/scripts/copy_es_snapshot_cache.sh b/.buildkite/scripts/copy_es_snapshot_cache.sh new file mode 100755 index 0000000000000..d2b325168482a --- /dev/null +++ b/.buildkite/scripts/copy_es_snapshot_cache.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# If cached snapshots are baked into the agent, copy them into our workspace first +# We are doing this rather than simply changing the ES base path because many workers +# run with the workspace mounted in memory or on a local ssd +cacheDir="$ES_CACHE_DIR/cache" +if [[ -d "$cacheDir" ]]; then + mkdir -p .es/cache + echo "--- Copying ES snapshot cache" + echo "Copying cached snapshots from $cacheDir to .es/cache" + cp -R "$cacheDir"/* .es/cache/ +fi diff --git a/.buildkite/scripts/steps/functional/common.sh b/.buildkite/scripts/steps/functional/common.sh index edb618f692093..5a58594404b15 100755 --- a/.buildkite/scripts/steps/functional/common.sh +++ b/.buildkite/scripts/steps/functional/common.sh @@ -8,17 +8,7 @@ source .buildkite/scripts/common/util.sh .buildkite/scripts/bootstrap.sh .buildkite/scripts/download_build_artifacts.sh - -# If cached snapshots are baked into the agent, copy them into our workspace first -# We are doing this rather than simply changing the ES base path because many workers -# run with the workspace mounted in memory or on a local ssd -cacheDir="$ES_CACHE_DIR/cache" -if [[ -d "$cacheDir" ]]; then - mkdir -p .es/cache - echo "--- Copying ES snapshot cache" - echo "Copying cached snapshots from $cacheDir to .es/cache" - cp -R "$cacheDir"/* .es/cache/ -fi +.buildkite/scripts/copy_es_snapshot_cache.sh is_test_execution_step diff --git a/.buildkite/scripts/steps/test/jest_integration.sh b/.buildkite/scripts/steps/test/jest_integration.sh index fd7b9a1d6ad54..83f9509f362ca 100755 --- a/.buildkite/scripts/steps/test/jest_integration.sh +++ b/.buildkite/scripts/steps/test/jest_integration.sh @@ -7,6 +7,7 @@ source .buildkite/scripts/common/util.sh is_test_execution_step .buildkite/scripts/bootstrap.sh +.buildkite/scripts/copy_es_snapshot_cache.sh echo '--- Jest Integration Tests' .buildkite/scripts/steps/test/jest_parallel.sh jest.integration.config.js diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index cfc4c0564962b..c5132fe51a678 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -237,6 +237,14 @@ you make the necessary updates after you upgrade to 8.15.0. The Uptime app is already hidden from Kibana when there is no recent Heartbeat data, but will be completely removed in 9.0.0. You should migrate to Synthetics as an alternative. For more details, refer to the {observability-guide}/uptime-intro.html[Uptime documentation]. ==== +[discrete] +.<> are deprecated in 8.15.0 and will be removed in a future version. +[%collapsible] +==== +*Details* + +Search sessions are now deprecated and will be removed in a future version. By default, queries that take longer than 10 minutes (the default for the advanced setting `search:timeout`) will be canceled. To allow queries to run longer, consider increasing `search:timeout` or setting it to `0` which will allow queries to continue running as long as a user is waiting on-screen for results. +==== + [float] [[breaking-changes-8.15.0]] === Breaking changes diff --git a/docs/api/spaces-management.asciidoc b/docs/api/spaces-management.asciidoc index 91b7ae349b9a8..7390a3815cd65 100644 --- a/docs/api/spaces-management.asciidoc +++ b/docs/api/spaces-management.asciidoc @@ -1,38 +1,4 @@ -[role="xpack"] [[spaces-api]] == {kib} spaces APIs -Manage your {kib} spaces. - -The following {kib} spaces APIs are available: - -* <> to create a {kib} space - -* <> to update an existing {kib} space - -* <> to retrieve a specified {kib} space - -* <> to retrieve all {kib} spaces - -* <> to delete a {kib} space - -* <> to copy saved objects between spaces - -* <> to overwrite saved objects returned as errors from the copy saved objects to space API - -* <> to disable legacy URL aliases if an error is encountered - -* <> to update one or more saved objects to add and/or remove them from specified spaces - -* <> to collect references and spaces context for saved objects - -include::spaces-management/post.asciidoc[] -include::spaces-management/put.asciidoc[] -include::spaces-management/get.asciidoc[] -include::spaces-management/get_all.asciidoc[] -include::spaces-management/delete.asciidoc[] -include::spaces-management/copy_saved_objects.asciidoc[] -include::spaces-management/resolve_copy_saved_objects_conflicts.asciidoc[] -include::spaces-management/disable_legacy_url_aliases.asciidoc[] -include::spaces-management/update_objects_spaces.asciidoc[] -include::spaces-management/get_shareable_references.asciidoc[] +For the latest details, refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. diff --git a/docs/api/spaces-management/copy_saved_objects.asciidoc b/docs/api/spaces-management/copy_saved_objects.asciidoc deleted file mode 100644 index 32f7480a39b87..0000000000000 --- a/docs/api/spaces-management/copy_saved_objects.asciidoc +++ /dev/null @@ -1,491 +0,0 @@ -[role="xpack"] -[[spaces-api-copy-saved-objects]] -=== Copy saved objects to space API -++++ -Copy saved objects to space -++++ - -experimental[] Copy saved objects between spaces. - -It also allows you to automatically copy related objects, so when you copy a `dashboard`, this can automatically copy over the -associated visualizations, {data-sources}, and saved searches, as required. - -You can request to overwrite any objects that already exist in the target space if they share an ID, or you can use the -<> to do this on a per-object basis. - -[[spaces-api-copy-saved-objects-request]] -==== {api-request-title} - -`POST :/api/spaces/_copy_saved_objects` - -`POST :/s//api/spaces/_copy_saved_objects` - -[[spaces-api-copy-saved-objects-path-params]] -==== {api-path-parms-title} - -`space_id`:: - (Optional, string) The ID of the space that contains the saved objects you want to copy. When `space_id` is unspecified in the URL, the - default space is used. - -[role="child_attributes"] -[[spaces-api-copy-saved-objects-request-body]] -==== {api-request-body-title} - -`spaces`:: - (Required, string array) The IDs of the spaces where you want to copy the specified objects. - -`objects`:: - (Required, object array) The saved objects to copy. -+ -.Properties of `objects` -[%collapsible%open] -===== - `type`::: - (Required, string) The saved object type. - - `id`::: - (Required, string) The saved object ID. -===== - -`includeReferences`:: - (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target - spaces. The default value is `false`. - -`createNewCopies`:: - (Optional, boolean) Creates new copies of saved objects, regenerates each object ID, and resets the origin. When used, potential conflict - errors are avoided. The default value is `true`. -+ -NOTE: This option cannot be used with the `overwrite` and `compatibilityMode` options. - -`overwrite`:: - (Optional, boolean) When set to `true`, all conflicts are automatically overridden. When a saved object with a matching `type` and `id` - exists in the target space, that version is replaced with the version from the source space. The default value is `false`. -+ -NOTE: This option cannot be used with the `createNewCopies` option. - -`compatibilityMode`:: - (Optional, boolean) Applies various adjustments to the saved objects that are being copied to maintain compatibility between different {kib} - versions. Use this option only if you encounter issues with copied saved objects. -+ -NOTE: This option cannot be used with the `createNewCopies` option. - -[[spaces-api-copy-saved-objects-response-codes]] -==== Response codes - -`200`:: - Indicates a successful call. - -`404`:: - Indicates that the request failed because one or more of the objects specified could not be found. A list of the unresolved objects are included in the 404 response attributes. - -[role="child_attributes"] -[[spaces-api-copy-saved-objects-response-body]] -==== {api-response-body-title} - -``:: - (object) An object that describes the result of the copy operation for the space. Includes the dynamic keys in the response. -+ -.Properties of `` -[%collapsible%open] -===== - `success`::: - (boolean) The copy operation was successful. When set to `false`, some objects may have been copied. For additional information, refer - to the `errors` and `successResults` properties. - - `successCount`::: - (number) The number of objects that successfully copied. - - `errors`::: - (Optional, array) The errors that occurred during the copy operation. When errors are reported, the `success` flag is set to `false`. -+ -NOTE: One object may result in multiple errors, which requires separate steps to resolve. For instance, a `missing_references` error and a -`conflict` error. -+ -.Properties of `errors` -[%collapsible%open] -====== - `id`:::: - (string) The saved object ID that failed to copy. - `type`:::: - (string) The type of saved object that failed to copy. - `error`:::: - (object) The error that caused the copy operation to fail. -+ -.Properties of `error` -[%collapsible%open] -======= - `type`:::: - (string) The type of error. For example, `conflict`, `ambiguous_conflict`, `missing_references`, `unsupported_type`, or `unknown`. - Errors marked as `conflict` or `ambiguous_conflict` may be resolved by using the <>. - `destinationId`:::: - (Optional, string) The destination ID that was used during the copy attempt. This is only present on `conflict` error types. - `destinations`:::: - (Optional, array) A list of possible object destinations with `id`, `title`, and `updatedAt` fields to describe each one. This is - only present on `ambiguous_conflict` error types. -======= -====== - - `successResults`::: - (Optional, array) Indicates successfully copied objects, with any applicable metadata. -+ -NOTE: Objects are created when all resolvable errors are addressed, including conflict and missing references errors. For more information, -refer to the <>. - -===== -[[spaces-api-copy-saved-objects-example]] -==== {api-examples-title} - -[[spaces-api-copy-saved-objects-example-1]] -===== Successful copy (with `createNewCopies` enabled) - -Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` space. In this example, -the dashboard has a reference to a visualization, and that has a reference to a {data-source}: - -[source,sh] ----- -$ curl -X POST api/spaces/_copy_saved_objects -{ - "objects": [{ - "type": "dashboard", - "id": "my-dashboard" - }], - "spaces": ["marketing"], - "includeReferences": true -} ----- -// KIBANA - -The API returns the following: - -[source,sh] ----- -{ - "marketing": { - "success": true, - "successCount": 3, - "successResults": [ - { - "id": "my-dashboard", - "type": "dashboard", - "destinationId": "1e127098-5b80-417f-b0f1-c60c8395358f", - "meta": { - "icon": "dashboardApp", - "title": "Look at my dashboard" - } - }, - { - "id": "my-vis", - "type": "visualization", - "destinationId": "a610ed80-1c73-4507-9e13-d3af736c8e04", - "meta": { - "icon": "visualizeApp", - "title": "Look at my visualization" - } - }, - { - "id": "my-index-pattern", - "type": "index-pattern", - "destinationId": "bc3c9c70-bf6f-4bec-b4ce-f4189aa9e26b", - "meta": { - "icon": "indexPatternApp", - "title": "my-pattern-*" - } - } - ] - } -} ----- - -The result indicates a successful copy, and all three objects are created. Since these objects were created as new copies, each entry in the -`successResults` array includes a `destinationId` attribute. - -[[spaces-api-copy-saved-objects-example-2]] -===== Successful copy (with `createNewCopies` disabled) - -Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` space. In this example, -the dashboard has a reference to a visualization, and that has a reference to a {data-source}: - -[source,sh] ----- -$ curl -X POST api/spaces/_copy_saved_objects -{ - "objects": [{ - "type": "dashboard", - "id": "my-dashboard" - }], - "spaces": ["marketing"], - "includeReferences": true, - "createNewCopies": false -} ----- -// KIBANA - -The API returns the following: - -[source,sh] ----- -{ - "marketing": { - "success": true, - "successCount": 3, - "successResults": [ - { - "id": "my-dashboard", - "type": "dashboard", - "meta": { - "icon": "dashboardApp", - "title": "Look at my dashboard" - } - }, - { - "id": "my-vis", - "type": "visualization", - "meta": { - "icon": "visualizeApp", - "title": "Look at my visualization" - } - }, - { - "id": "my-index-pattern", - "type": "index-pattern", - "meta": { - "icon": "indexPatternApp", - "title": "my-pattern-*" - } - } - ] - } -} ----- - -The result indicates a successful copy, and all three objects are created. - -[[spaces-api-copy-saved-objects-example-3]] -===== Failed copy (with conflict errors) - -Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` and `sales` spaces. In -this example, the dashboard has a reference to a visualization and a *Canvas* workpad, and the visualization has a reference to an index -pattern: - -[source,sh] ----- -$ curl -X POST api/spaces/_copy_saved_objects -{ - "objects": [{ - "type": "dashboard", - "id": "my-dashboard" - }], - "spaces": ["marketing", "sales"], - "includeReferences": true, - "createNewCopies": false -} ----- -// KIBANA - -The API returns the following: - -[source,sh] ----- -{ - "marketing": { - "success": true, - "successCount": 4, - "successResults": [ - { - "id": "my-dashboard", - "type": "dashboard", - "meta": { - "icon": "dashboardApp", - "title": "Look at my dashboard" - } - }, - { - "id": "my-vis", - "type": "visualization", - "meta": { - "icon": "visualizeApp", - "title": "Look at my visualization" - } - }, - { - "id": "my-canvas", - "type": "canvas-workpad", - "meta": { - "icon": "canvasApp", - "title": "Look at my canvas" - } - }, - { - "id": "my-index-pattern", - "type": "index-pattern", - "meta": { - "icon": "indexPatternApp", - "title": "my-pattern-*" - } - } - ] - }, - "sales": { - "success": false, - "successCount": 1, - "errors": [ - { - "id": "my-pattern", - "type": "index-pattern", - "title": "my-pattern-*", - "error": { - "type": "conflict" - }, - "meta": { - "icon": "indexPatternApp", - "title": "my-pattern-*" - } - }, - { - "id": "my-visualization", - "type": "my-vis", - "title": "Look at my visualization", - "error": { - "type": "conflict", - "destinationId": "another-vis" - }, - "meta": { - "icon": "visualizeApp", - "title": "Look at my visualization" - } - }, - { - "id": "my-canvas", - "type": "canvas-workpad", - "title": "Look at my canvas", - "error": { - "type": "ambiguous_conflict", - "destinations": [ - { - "id": "another-canvas", - "title": "Look at another canvas", - "updatedAt": "2020-07-08T16:36:32.377Z" - }, - { - "id": "yet-another-canvas", - "title": "Look at yet another canvas", - "updatedAt": "2020-07-05T12:29:54.849Z" - } - ] - }, - "meta": { - "icon": "canvasApp", - "title": "Look at my canvas" - } - } - ], - "successResults": [ - { - "id": "my-dashboard", - "type": "dashboard", - "meta": { - "icon": "dashboardApp", - "title": "Look at my dashboard" - } - } - ] - } -} ----- - -The result indicates a successful copy for the `marketing` space, and an unsuccessful copy for the `sales` space because the {data-source}, -visualization, and *Canvas* workpad each resulted in a conflict error: - -* A {data-source} with the same ID already exists, which resulted in a conflict error. To resolve the error, overwrite the existing object, -or skip the object. - -* A visualization with a different ID, but the same origin already exists, which resulted in a conflict error. The `destinationId` field -contains the `id` of the other visualization, which caused the conflict. The behavior is added to make sure that new objects that can be -shared between spaces behave in a similar way as legacy non-shareable objects. When a shareable object is copied into a new space, it -retains its origin so that the conflicts are encountered as expected. To resolve, overwrite the specified destination object, or skip the -object. - -* Two *Canvas* workpads with different IDs, but the same origin, already exist, which resulted in a conflict error. The `destinations` array -describes the other workpads which caused the conflict. When a shareable object is copied into a new space, then shared to another space -where an object of the same origin exists, the conflict error occurs. To resolve, pick a destination object to overwrite, or skip the -object. - -Objects are created when the error is resolved using the <>. - -[[spaces-api-copy-saved-objects-example-4]] -===== Failed copy (with missing reference errors) - -Copy a dashboard with the `my-dashboard` ID, including all references from the `default` space to the `marketing` space. In this example, -the dashboard has a reference to a visualization and a *Canvas* workpad, and the visualization has a reference to a {data-source}: - -[source,sh] ----- -$ curl -X POST api/spaces/_copy_saved_objects -{ - "objects": [{ - "type": "dashboard", - "id": "my-dashboard" - }], - "spaces": ["marketing"], - "includeReferences": true, - "createNewCopies": false -} ----- -// KIBANA - -The API returns the following: - -[source,sh] ----- -{ - "marketing": { - "success": false, - "successCount": 2, - "errors": [ - { - "id": "my-vis", - "type": "visualization", - "title": "Look at my visualization", - "error": { - "type": "missing_references", - "references": [ - { - "type": "index-pattern", - "id": "my-pattern-*" - } - ] - }, - "meta": { - "icon": "visualizeApp", - "title": "Look at my visualization" - } - }, - ] - "successResults": [ - { - "id": "my-dashboard", - "type": "dashboard", - "meta": { - "icon": "dashboardApp", - "title": "Look at my dashboard" - } - }, - { - "id": "my-canvas", - "type": "canvas-workpad", - "meta": { - "icon": "canvasApp", - "title": "Look at my canvas" - } - } - ], - } -} ----- - -The result indicates an unsuccessful copy because the visualization resulted in a missing references error. - -Objects are created when the errors are resolved using the <>. diff --git a/docs/api/spaces-management/delete.asciidoc b/docs/api/spaces-management/delete.asciidoc deleted file mode 100644 index b1b453a0e2d3e..0000000000000 --- a/docs/api/spaces-management/delete.asciidoc +++ /dev/null @@ -1,23 +0,0 @@ -[[spaces-api-delete]] -=== Delete space API -++++ -Delete space -++++ - -experimental[] Delete a {kib} space. - -WARNING: When you delete a space, all saved objects that belong to the space are automatically deleted, which is permanent and cannot be undone. - -[[spaces-api-delete-request]] -==== Request - -`DELETE :/api/spaces/space/` - -[[spaces-api-delete-errors-codes]] -==== Response codes - -`204`:: - Indicates a successful call. - -`404`:: - Indicates that the request failed. diff --git a/docs/api/spaces-management/disable_legacy_url_aliases.asciidoc b/docs/api/spaces-management/disable_legacy_url_aliases.asciidoc deleted file mode 100644 index 3f713d9d0c25e..0000000000000 --- a/docs/api/spaces-management/disable_legacy_url_aliases.asciidoc +++ /dev/null @@ -1,59 +0,0 @@ -[[spaces-api-disable-legacy-url-aliases]] -=== Disable legacy URL aliases API -++++ -Disable legacy URL aliases -++++ - -experimental[] Disable a <> in {kib}. - -[[spaces-api-disable-legacy-url-aliases-request]] -==== {api-request-title} - -`POST :/api/spaces/_disable_legacy_url_aliases` - -[role="child_attributes"] -[[spaces-api-disable-legacy-url-aliases-request-body]] -==== {api-request-body-title} - -`aliases`:: - (Required, object array) The aliases to disable. -+ -.Properties of `aliases` -[%collapsible%open] -===== - `targetSpace`::: - (Required, string) The space where the alias target object exists. - - `targetType`::: - (Required, string) The type of the alias target object. - - `sourceId`::: - (Required, string) The ID of the alias source object. This is the "legacy" object ID. -===== - -[[spaces-api-disable-legacy-url-aliases-response-codes]] -==== {api-response-codes-title} - -`204`:: - Indicates a successful call. - -[[spaces-api-disable-legacy-url-aliases-example]] -==== {api-examples-title} - -[source,sh] --------------------------------------------------- -$ curl -X POST api/spaces/_disable_legacy_url_aliases -{ - "aliases": [ - { - "targetSpace": "bills-space", - "targetType": "dashboard", - "sourceId": "123" - } - ] -} --------------------------------------------------- -// KIBANA - -This example leaves the alias intact, but the legacy URL for this alias, http://localhost:5601/s/bills-space/app/dashboards#/view/123, will -no longer function. The dashboard still exists, and you can access it with the new URL. \ No newline at end of file diff --git a/docs/api/spaces-management/get.asciidoc b/docs/api/spaces-management/get.asciidoc deleted file mode 100644 index f3e3462b2e0da..0000000000000 --- a/docs/api/spaces-management/get.asciidoc +++ /dev/null @@ -1,37 +0,0 @@ -[[spaces-api-get]] -=== Get a space API -++++ -Get space -++++ - -experimental[] Retrieve a specified {kib} space. - -[[spaces-api-get-request]] -==== Request - -`GET :/api/spaces/space/marketing` - -[[spaces-api-get-response-codes]] -==== Response code - -`200`:: - Indicates a successful call. - -[[spaces-api-get-example]] -==== Example - -The API returns the following: - -[source,sh] --------------------------------------------------- -{ - "id": "marketing", - "name": "Marketing", - "description" : "This is the Marketing Space", - "color": "#aabbcc", - "initials": "MK", - "disabledFeatures": [], - "imageUrl": "", - "solution": "es" -} --------------------------------------------------- diff --git a/docs/api/spaces-management/get_all.asciidoc b/docs/api/spaces-management/get_all.asciidoc deleted file mode 100644 index 92a65d670ad87..0000000000000 --- a/docs/api/spaces-management/get_all.asciidoc +++ /dev/null @@ -1,138 +0,0 @@ -[[spaces-api-get-all]] -=== Get all {kib} spaces API -++++ -Get all spaces -++++ - -experimental[] Retrieve all {kib} spaces. - -[[spaces-api-get-all-request]] -==== Request - -`GET :/api/spaces/space` - -[[spaces-api-get-all-query-params]] -==== Query parameters - -`purpose`:: - (Optional, string) Valid options include `any`, `copySavedObjectsIntoSpace`, and `shareSavedObjectsIntoSpace`. This determines what - authorization checks are applied to the API call. If `purpose` is not provided in the URL, the `any` purpose is used. - -`include_authorized_purposes`:: - (Optional, boolean) When enabled, the API will return any spaces that the user is authorized to access in any capacity, and each space - will contain the purpose(s) for which the user is authorized. This can be useful to determine which spaces a user can read but not take a - specific action in. If the Security plugin is not enabled, this will have no effect, as no authorization checks would take place. -+ -NOTE: This option cannot be used in conjunction with `purpose`. - -[[spaces-api-get-all-response-codes]] -==== Response code - -`200`:: - Indicates a successful call. - -[[spaces-api-get-all-example]] -==== Examples - -[[spaces-api-get-all-example-1]] -===== Default options - -Retrieve all spaces without specifying any options: - -[source,sh] --------------------------------------------------- -$ curl -X GET api/spaces/space --------------------------------------------------- - -The API returns the following: - -[source,sh] --------------------------------------------------- -[ - { - "id": "default", - "name": "Default", - "description" : "This is the Default Space", - "disabledFeatures": [], - "imageUrl": "", - "_reserved": true - }, - { - "id": "marketing", - "name": "Marketing", - "description" : "This is the Marketing Space", - "color": "#aabbcc", - "disabledFeatures": ["apm"], - "initials": "MK", - "imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSU" - }, - { - "id": "sales", - "name": "Sales", - "initials": "MK", - "disabledFeatures": ["discover"], - "imageUrl": "", - "solution": "oblt" - } -] --------------------------------------------------- - -[[spaces-api-get-all-example-2]] -===== Custom options - -The user has read-only access to the Sales space. Retrieve all spaces and specify options: - -[source,sh] --------------------------------------------------- -$ curl -X GET api/spaces/space?purpose=shareSavedObjectsIntoSpace&include_authorized_purposes=true --------------------------------------------------- - -The API returns the following: - -[source,sh] --------------------------------------------------- -[ - { - "id": "default", - "name": "Default", - "description" : "This is the Default Space", - "disabledFeatures": [], - "imageUrl": "", - "_reserved": true, - "authorizedPurposes": { - "any": true, - "copySavedObjectsIntoSpace": true, - "findSavedObjects": true, - "shareSavedObjectsIntoSpace": true, - } - }, - { - "id": "marketing", - "name": "Marketing", - "description" : "This is the Marketing Space", - "color": "#aabbcc", - "disabledFeatures": ["apm"], - "initials": "MK", - "imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSU", - "authorizedPurposes": { - "any": true, - "copySavedObjectsIntoSpace": true, - "findSavedObjects": true, - "shareSavedObjectsIntoSpace": true, - } - }, - { - "id": "sales", - "name": "Sales", - "initials": "MK", - "disabledFeatures": ["discover"], - "imageUrl": "", - "authorizedPurposes": { - "any": true, - "copySavedObjectsIntoSpace": false, - "findSavedObjects": true, - "shareSavedObjectsIntoSpace": false, - } - } -] --------------------------------------------------- diff --git a/docs/api/spaces-management/get_shareable_references.asciidoc b/docs/api/spaces-management/get_shareable_references.asciidoc deleted file mode 100644 index 8066736c0c15d..0000000000000 --- a/docs/api/spaces-management/get_shareable_references.asciidoc +++ /dev/null @@ -1,81 +0,0 @@ -[role="xpack"] -[[spaces-api-get-shareable-references]] -=== Get shareable references API -++++ -Get shareable references -++++ - -experimental[] Get shareable references. - -Collects references and spaces context for saved objects. - -[[spaces-api-get-shareable-references-request]] -==== {api-request-title} - -`POST :/api/spaces/_get_shareable_references` - -[[spaces-api-get-shareable-references-request-body]] -==== {api-request-body-title} - -`objects`:: - (Required, object array) The saved objects to collect outbound references for. -+ -.Properties of `objects` -[%collapsible%open] -===== - `type`::: - (Required, string) The saved object type. - - `id`::: - (Required, string) The saved object ID. -===== - -[role="child_attributes"] -[[spaces-api-get-shareable-references-response-body]] -==== {api-response-body-title} - -`objects`:: - (object array) The returned input object or one of its references, with additional context. -+ -.Properties of `objects` -[%collapsible%open] -===== - `type`::: - (string) The saved object type. - - `id`::: - (string) The saved object ID. - - `originId`::: - (string) The origin ID of the referenced object (if it has one). - - `inboundReferences`::: - (object array) References to this object. -+ -NOTE: This does not contain _all inbound references everywhere_, it only contains inbound references to this object within the scope of this operation. -+ -.Properties of `inboundReferences` -[%collapsible%open] -====== - `type`:::: - (string) The type of the object that has the inbound reference. - - `id`:::: - (string) The ID of the object that has the inbound reference. - - `name`:::: - (string) The name of the inbound reference. -====== - - `spaces`::: - (string array) The space(s) that the referenced saved object exists in. - - `spacesWithMatchingAliases`::: - (string array) The space(s) that legacy URL aliases matching this type/id exist in. (if there are any) - - `spacesWithMatchingOrigins`::: - (string array) The space(s) that objects matching this origin exist in (including this one). (if there are any) - - `isMissing`::: - (boolean) Whether or not this object or reference is missing. -===== diff --git a/docs/api/spaces-management/post.asciidoc b/docs/api/spaces-management/post.asciidoc deleted file mode 100644 index 4c5976249f80e..0000000000000 --- a/docs/api/spaces-management/post.asciidoc +++ /dev/null @@ -1,64 +0,0 @@ -[[spaces-api-post]] -=== Create space API -++++ -Create space -++++ - -experimental[] Create a {kib} space. - -[[spaces-api-post-request]] -==== Request - -`POST :/api/spaces/space` - -[[spaces-api-post-request-body]] -==== Request body - -`id`:: - (Required, string) The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, '_', and '-'). You are unable to change the ID with the update operation. - -`name`:: - (Required, string) The display name for the space. - -`description`:: - (Optional, string) The description for the space. - -`disabledFeatures`:: - (Optional, string array) The list of disabled features for the space. To get a list of available feature IDs, use the <>. - -`initials`:: - (Optional, string) The initials shown in the space avatar. By default, the initials are automatically generated from the space name. Initials must be 1 or 2 characters. - -`color`:: - (Optional, string) The hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name. - -`imageUrl`:: - (Optional, string) The data-URL encoded image to display in the space avatar. If specified, `initials` will not be displayed, and the `color` will be visible as the background color for transparent images. - For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images. - -`solution`:: - (Optional, string) The solution defined for the space. Can be one of `security`, `oblt`, `es`, `classic` - -[[spaces-api-post-response-codes]] -==== Response codes - -`200`:: - Indicates a successful call. - -[[spaces-api-post-example]] -==== Example - -[source,sh] --------------------------------------------------- -$ curl -X POST api/spaces/space -{ - "id": "marketing", - "name": "Marketing", - "description" : "This is the Marketing Space", - "color": "#aabbcc", - "initials": "MK", - "disabledFeatures": [], - "imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABACAYAAABC6cT1AAAGf0lEQVRoQ+3abYydRRUH8N882xYo0IqagEVjokQJKAiKBjXExC9G/aCkGowCIghCkRcrVSSKIu/FEiqgGL6gBIlAYrAqUTH6hZgQFVEMKlQFfItWoQWhZe8z5uzMLdvbfbkLxb13d+fbvfe588x/zpn/+Z9zJpmnI81T3BaAzzfLL1h8weLzZAcWXH2eGHo7zAWLL1h8nuzAjFw9G1N6Kzq8HnuM36MR8iibF3Fv4q+7cv8yDV6K13bYq2furSP8Ag8ncr/vnSnwRViJT2GfCV7yL1yHGxLb+l3EdM9lluNEnIC9xz+f2ZL4Er6Z2DrdXN3fZwp8CU7OfDHxggle8lTLbQ1nJ/7Z7yKmey5zYGZt4h2IzR8/trRc2PDlxJPTzfVcgJ+CC0wMPOa9F6cm7up3EVM9V9386MxliVdM8GwAv6hh/awCz/w7lY25OtF5ruBz4ZLP42NYNrDAFbC3YPWuILnMAfgq3oaRQQYea/stViV+sgssvjKzLvGySeaaNVfP4d7Btokgvxj/bblgpueuF1hmWcyTCmfE3J3M1lTcv0vMswM88zR+jpw4osu6me8kzkpsfLZWzxyRuabO22buxxOJ12FxnXfWgEe83pB5sOE47BsLymzscOoi7nw2JJfZreUjiUsTyzKPZm5NvBDvSuw268AzNzV8H5/Am+qCnsAXgpgSW2Zq9cyKlksbPlTd+te4quWNieMHBfiNDdciYnwsdI/MaOaWhnMTf54J8CqNj8x8JXFIZltYu+HqlmNT8YSBsHgAPw/vxvlVV4du/s0oaxbxg0TbL/jMni0nNcVjQq7+HZfgtpbzBg342TgQ63AkmsymxBW4IjE6A+D7Vzd/fyWxIM/VuCe+HzTgZ2Jpy/kNJ2FJLmLm24mPJ/42A+Bvrxt4SISwlhsaPodH26LZB8rVA3inwwebsrixJCZzX+KMxI/7AV61eVh3DV6Mx3EOvh4kN6jAg8nfUCXm4d1wE66OyxNPTQc+s3/o/MoXizL3JE5O3F3P/uBZPPF4Zr+Wi5uSO48ZPRdyCwn7YB/A35m5KhWNHox4fcNnIs0ddOCRSBxf8+cQG+Huf0l8NJVYP+nI7NXy2ar4QqIGm69JfKPOE2w/mBavCzwM11R2D+ChsUO7hyUfmwx55qDM1xJvqZ7y08TpifuGBfjeURVJnNIVGpkNiXNS0ds7jcySDitDCCWW56LJ10fRo8sNA+3qXUSZD2CtQlZh9T+1rB7h9oliembflnMbzqgSNZKbKGHdPm7OwXb1CvQ1metSETMpszmzvikCJNh/h5E5PHNl4qga/+/cxqrdeWDYgIe7X5L4cGJPJX2940lOX8pD41FnFnc4riluvQKbK0dcHJFi2IBHNTQSlguru4d2/wPOTNzRA3x5y+U1E1uqWDkETOT026XuUJzx6u7ReLhSYenQ7uHua0fKZmwfmcPqsQjxE5WVONcRxn7X89zgn/EKPMRMxOVQXmP18Mx3q3b/Y/0cQE/IhFtHESMsHFlZ1Ml3CH3DZPHImY+pxcKumNmYirtvqMBfhMuU6s3iqOQkTsMPe1tCQwO8Ajs0lxr7W+vnp1MJc9EgCNd/cy6x+9D4veXmprj5wxMw/3C4egW6zzgZOlYZzfwo3F2J7ael0pJamvlPKgWNKFft1AAcKotXoFEbD7kaoSoQPVKB35+5KHF0lai/rJo+up87jWEE/qqqwY+qrL21LWLm95lPJ16ppKw31XC3PXYPJauPEx7B6BHCgrSizRs18qiaRp8tlN3ueCTYPHH9RNaunjI8Z7wLYpT3jZSCYXQ8e9vTsRE/q+no3XMKeObgGtaintbb/AvXj4JDkNw/5hrwYPfIvlZFUbLn7G5q+eQIN09Vnho6cqvnM/Lt99RixH49wO8K0ZL41WTWHoQzvsNVkOheZqKhEGpsp3SzB+BBtZAYve7uOR9tuTaaB6l0XScdYfEQPpkTUyHEGP+XqyDBzu+NBCITUjNWHynkrbWKOuWFn1xKzqsyx0bdvS78odp0+N503Zao0uCsWuSIDku8/7EO60b41vN5+Ses9BKlTdvd8bhp9EBvJjWJAIn/vxwHe6b3tSk6JFPV4nq85oAOrx555v/x/rh3E6Lo+bnuNS4uB4Cuq0ZfvO8X1rM6q/+vnjLVqZq7v83onttc2oYF4HPJmv1gWbB4P7s0l55ZsPhcsmY/WBYs3s8uzaVn5q3F/wf70mRuBCtbjQAAAABJRU5ErkJggg==" -} --------------------------------------------------- -// KIBANA diff --git a/docs/api/spaces-management/put.asciidoc b/docs/api/spaces-management/put.asciidoc deleted file mode 100644 index 88d0d41114eb5..0000000000000 --- a/docs/api/spaces-management/put.asciidoc +++ /dev/null @@ -1,64 +0,0 @@ -[[spaces-api-put]] -=== Update space API -++++ -Update space -++++ - -experimental[] Update an existing {kib} space. - -[[spaces-api-put-api-request]] -==== Request - -`PUT :/api/spaces/space/` - -[[spaces-api-put-request-body]] -==== Request body - -`id`:: - (Required, string) The space ID that is part of the {kib} URL when inside the space. You are unable to change the ID with the update operation. - -`name`:: - (Required, string) The display name for the space. - -`description`:: - (Optional, string) The description for the space. - -`disabledFeatures`:: - (Optional, string array) The list of disabled features for the space. To get a list of available feature IDs, use the <>. - -`initials`:: - (Optional, string) Specifies the initials shown in the space avatar. By default, the initials are automatically generated from the space name. Initials must be 1 or 2 characters. - -`color`:: - (Optional, string) Specifies the hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name. - -`imageUrl`:: - (Optional, string) Specifies the data-url encoded image to display in the space avatar. If specified, `initials` will not be displayed, and the `color` will be visible as the background color for transparent images. - For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images. - -`solution`:: - (Optional, string) The solution defined for the space. Can be one of `security`, `oblt`, `es`, `classic`. - -[[spaces-api-put-response-codes]] -==== Response codes - -`200`:: - Indicates a successful call. - -[[sample-api-example]] -==== Example - -[source,sh] --------------------------------------------------- -$ curl -X PUT api/spaces/space/marketing -{ - "id": "marketing", - "name": "Marketing", - "description" : "This is the Marketing Space", - "color": "#aabbcc", - "initials": "MK", - "disabledFeatures": [], - "imageUrl": "" -} --------------------------------------------------- -// KIBANA diff --git a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc deleted file mode 100644 index 0ca5c72070a86..0000000000000 --- a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc +++ /dev/null @@ -1,317 +0,0 @@ -[role="xpack"] -[[spaces-api-resolve-copy-saved-objects-conflicts]] -=== Resolve copy saved objects to space conflicts API -++++ -Resolve copy to space conflicts -++++ - -experimental[] Overwrite saved objects that are returned as errors from the <>. - -[[spaces-api-resolve-copy-saved-objects-conflicts-request]] -==== {api-request-title} - -`POST :/api/spaces/_resolve_copy_saved_objects_errors` - -`POST :/s//api/spaces/_resolve_copy_saved_objects_errors` - -[[spaces-api-resolve-copy-saved-objects-conflicts-prereqs]] -==== {api-prereq-title} - -Execute the <>, which returns the errors for you to resolve. - -[[spaces-api-resolve-copy-saved-objects-conflicts-path-params]] -==== {api-path-parms-title} - -`space_id`:: -(Optional, string) The ID of the space that contains the saved objects you want to copy. When `space_id` is unspecified in the URL, the default space is used. The `space_id` must be the same value used during the failed <> operation. - -[role="child_attributes"] -[[spaces-api-resolve-copy-saved-objects-conflicts-request-body]] -==== {api-request-body-title} - -`objects`:: - (Required, object array) The saved objects to copy. The `objects` must be the same values used during the failed <> operation. -+ -.Properties of `objects` -[%collapsible%open] -===== - `type`::: - (Required, string) The saved object type. - - `id`::: - (Required, string) The saved object ID. -===== - -`includeReferences`:: - (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects are copied into the target spaces. The `includeReferences` must be the same values used during the failed <> operation. The default value is `false`. - -`createNewCopies`:: - (Optional, boolean) Creates new copies of the saved objects, regenerates each object ID, and resets the origin. When enabled during the - initial copy, also enable when resolving copy errors. The default value is `true`. - -`retries`:: - (Required, object) The retry operations to attempt, which can specify how to resolve different types of errors. Object keys represent the - target space IDs. -+ -.Properties of `retries` -[%collapsible%open] -===== - ``::: - (Required, array) The errors to resolve for the specified ``. -+ - -.Properties of `` -[%collapsible%open] -====== - `type`:::: - (Required, string) The saved object type. - `id`:::: - (Required, string) The saved object ID. - `overwrite`:::: - (Required, boolean) When set to `true`, the saved object from the source space (designated by the <>) overwrites the conflicting object in the destination space. When set to `false`, this does nothing. - `destinationId`:::: - (Optional, string) Specifies the destination ID that the copied object should have, if different from the current ID. - `ignoreMissingReferences`::: - (Optional, boolean) When set to `true`, any missing references errors are ignored. When set to `false`, does nothing. -====== -===== - -[role="child_attributes"] -[[spaces-api-resolve-copy-saved-objects-conflicts-response-body]] -==== {api-response-body-title} - -``:: - (object) An object that describes the result of the copy operation for the space. Includes the dynamic keys in the response. -+ -.Properties of `` -[%collapsible%open] -===== - `success`::: - (boolean) The copy operation was successful. When set to `false`, some objects may have been copied. For additional information, refer to the `successCount` and `errors` properties. - - `successCount`::: - (number) The number of objects that successfully copied. - - `errors`::: - (Optional, array) The errors that occurred during the copy operation. When errors are reported, the `success` flag is set to `false`. -+ -NOTE: One object may result in multiple errors, which requires separate steps to resolve. For instance, a `missing_references` error and a -`conflict` error. -+ - -.Properties of `errors` -[%collapsible%open] -====== - `id`:::: - (string) The saved object ID that failed to copy. - - `type`:::: - (string) The type of saved object that failed to copy. - - `error`:::: - (object) The error that caused the copy operation to fail. -+ - -.Properties of `error` -[%collapsible%open] -======= - `type`:::: - (string) The type of error. For example, `conflict`, `ambiguous_conflict`, `missing_references`, `unsupported_type`, or `unknown`. - `destinationId`:::: - (Optional, string) The destination ID that was used during the copy attempt. This is only present on `conflict` errors types. - `destinations`:::: - (Optional, array) A list of possible object destinations with `id`, `title`, and `updatedAt` fields to describe each one. This is - only present on `ambiguous_conflict` error types. -======= -====== - -`successResults`::: - (Optional, array) Indicates successfully copied objects, with any applicable metadata. -+ -NOTE: Objects are created when all resolvable errors are addressed, including conflict and missing references errors. For more information, -refer to the <>. - -===== - -[[spaces-api-resolve-copy-saved-objects-conflicts-example]] -==== {api-examples-title} - -[[spaces-api-resolve-copy-saved-objects-conflicts-example-1]] -===== Resolve conflict errors - -This example builds upon the <>. - -Resolve conflict errors for a {data-source}, visualization, and *Canvas* workpad by overwriting the existing saved objects: - -[source,sh] ----- -$ curl -X POST api/spaces/_resolve_copy_saved_objects_errors -{ - "objects": [{ - "type": "dashboard", - "id": "my-dashboard" - }], - "includeReferences": true, - "createNewCopies": false, - "retries": { - "sales": [ - { - "type": "index-pattern", - "id": "my-pattern", - "overwrite": true - }, - { - "type": "visualization", - "id": "my-vis", - "overwrite": true, - "destinationId": "another-vis" - }, - { - "type": "canvas", - "id": "my-canvas", - "overwrite": true, - "destinationId": "yet-another-canvas" - }, - { - "type": "dashboard", - "id": "my-dashboard" - } - ] - } -} ----- -// KIBANA - -The API returns the following: - -[source,sh] ----- -{ - "sales": { - "success": true, - "successCount": 4, - "successResults": [ - { - "id": "my-pattern", - "type": "index-pattern", - "meta": { - "icon": "indexPatternApp", - "title": "my-pattern-*" - } - }, - { - "id": "my-vis", - "type": "visualization", - "destinationId": "another-vis", - "meta": { - "icon": "visualizeApp", - "title": "Look at my visualization" - } - }, - { - "id": "my-canvas", - "type": "canvas-workpad", - "destinationId": "yet-another-canvas", - "meta": { - "icon": "canvasApp", - "title": "Look at my canvas" - } - }, - { - "id": "my-dashboard", - "type": "dashboard", - "meta": { - "icon": "dashboardApp", - "title": "Look at my dashboard" - } - } - ] - } -} ----- - -The result indicates a successful copy, and all four objects are created. - -TIP: If a prior copy attempt resulted in resolvable errors, you must include a retry for each object you want to copy, including any that -were returned in the `successResults` array. In this example, we retried copying the dashboard accordingly. - -[[spaces-api-resolve-copy-saved-objects-conflicts-example-2]] -===== Resolve missing reference errors - -This example builds upon the <>. - -Resolve missing reference errors for a visualization by ignoring the error: - -[source,sh] ----- -$ curl -X POST api/spaces/_resolve_copy_saved_objects_errors -{ - "objects": [{ - "type": "dashboard", - "id": "my-dashboard" - }], - "includeReferences": true, - "createNewCopies": false, - "retries": { - "marketing": [ - { - "type": "visualization", - "id": "my-vis", - "ignoreMissingReferences": true - }, - { - "type": "canvas", - "id": "my-canvas" - }, - { - "type": "dashboard", - "id": "my-dashboard" - } - ] - } -} ----- -// KIBANA - -The API returns the following: - -[source,sh] ----- -{ - "marketing": { - "success": true, - "successCount": 3, - "successResults": [ - { - "id": "my-vis", - "type": "visualization", - "meta": { - "icon": "visualizeApp", - "title": "Look at my visualization" - } - }, - { - "id": "my-canvas", - "type": "canvas-workpad", - "meta": { - "icon": "canvasApp", - "title": "Look at my canvas" - } - }, - { - "id": "my-dashboard", - "type": "dashboard", - "meta": { - "icon": "dashboardApp", - "title": "Look at my dashboard" - } - } - ] - } -} ----- - -The result indicates a successful copy and all three objects are created. - -TIP: If a prior copy attempt resulted in resolvable errors, you must include a retry for each object you want to copy, including any that -were returned in the `successResults` array. In this example, we retried copying the dashboard and canvas accordingly. diff --git a/docs/api/spaces-management/update_objects_spaces.asciidoc b/docs/api/spaces-management/update_objects_spaces.asciidoc deleted file mode 100644 index dec846fd6fee0..0000000000000 --- a/docs/api/spaces-management/update_objects_spaces.asciidoc +++ /dev/null @@ -1,142 +0,0 @@ -[role="xpack"] -[[spaces-api-update-objects-spaces]] -=== Update saved objects spaces API -++++ -Update saved objects spaces -++++ - -experimental[] Update saved objects spaces. - -Updates one or more saved objects to add and/or remove them from specified spaces. - -[[spaces-api-update-objects-spaces-request]] -==== {api-request-title} - -`POST :/api/spaces/_update_objects_spaces` - -[[spaces-api-update-objects-spaces-request-body]] -==== {api-request-body-title} - -`objects`:: - (Required, object array) The saved objects to update. -+ -.Properties of `objects` -[%collapsible%open] -===== - `type`::: - (Required, string) The saved object type. - - `id`::: - (Required, string) The saved object ID. -===== - -`spacesToAdd`:: - (Required, string array) The IDs of the spaces the specified objects should be added to. - -`spacesToRemove`:: - (Required, string array) The IDs of the spaces the specified objects should be removed from. - -[role="child_attributes"] -[[spaces-api-update-objects-spaces-response-body]] -==== {api-response-body-title} - -`objects`:: - (object array) The saved objects that have been updated. -+ -.Properties of `objects` -[%collapsible%open] -===== - `type`::: - (string) The saved object type. - - `id`::: - (string) The saved object ID. - - `spaces`::: - (string array) The space(s) that the referenced saved object exists in. - - `errors`::: - (string) Included if there was an error updating this object's spaces. -===== - -[[spaces-api-update-objects-spaces-example]] -==== {api-examples-title} - -[[spaces-api-update-objects-spaces-example-1]] -===== Sharing saved objects - -To share a saved object to a space programmatically follow these steps: - -1. Collect reference graph and spaces context for each saved object that you want to share using <>: -+ -[source,sh] ----- -$ curl -X POST /api/spaces/_get_shareable_references -{ - "objects": [ - { - "type": "index-pattern", - "id": "90943e30-9a47-11e8-b64d-95841ca0b247" - } - ] -} ----- -+ -The API returns the following: -+ -[source,json] ----- -{ - "objects": [ - { - "type": "index-pattern", - "id": "90943e30-9a47-11e8-b64d-95841ca0b247", - "spaces": ["default"], - "inboundReferences": [], - "spacesWithMatchingOrigins": ["default"] - } - ] -} ----- - -2. Check each saved object for `spacesWithMatchingOrigins` conflicts. -+ -Objects should not be shared to spaces with matching origins or you will create URL conflicts (causing the same URL to point to different saved objects). - -3. Check each saved object for `spacesWithMatchingAliases` conflicts. -+ -If these match the space(s) that these saved objects will be shared to you should disable legacy URL aliases for them using <>. -+ -When sharing to all spaces (`*`) all entries in `spacesWithMatchingAliases` should be checked. - -4. Update spaces of each saved object and all its references: -+ -[source,sh] ----- -$ curl -X POST /api/spaces/_update_objects_spaces -{ - "objects": [ - { - "type": "index-pattern", - "id": "90943e30-9a47-11e8-b64d-95841ca0b247" - } - ], - "spacesToAdd": ["test"], - "spacesToRemove": [] -} ----- -+ -The API returns the following: -+ -[source,json] ----- -{ - "objects": [ - { - "type": "index-pattern", - "id": "90943e30-9a47-11e8-b64d-95841ca0b247", - "spaces": ["default", "test"] - } - ] -} ----- diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index df96721205d59..231843081e7e1 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -100,7 +100,6 @@ into an older version of {kib}. For example: [float] -[role="xpack"] [[managing-saved-objects-copy-to-space]] === Copy to other {kib} spaces @@ -117,7 +116,6 @@ the saved object. If you don't want this behavior, use the <> instead. [float] -[role="xpack"] [[managing-saved-objects-share-to-space]] === Share to other {kib} spaces @@ -135,4 +133,79 @@ those space icons to open the Share UI. + The share operation automatically includes child objects that are related to the saved objects. -include::saved-objects/saved-object-ids.asciidoc[] +[[spaces-api-update-objects-spaces-example-1]] +To share a saved object to a space programmatically with the {api-kibana}/group/endpoint-spaces[spaces APIs], follow these steps: + +1. Collect reference graph and spaces context for each saved object that you want to share using get shareable references API: ++ +[source,sh] +---- +$ curl -X POST /api/spaces/_get_shareable_references +{ + "objects": [ + { + "type": "index-pattern", + "id": "90943e30-9a47-11e8-b64d-95841ca0b247" + } + ] +} +---- ++ +The API returns the following: ++ +[source,json] +---- +{ + "objects": [ + { + "type": "index-pattern", + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "spaces": ["default"], + "inboundReferences": [], + "spacesWithMatchingOrigins": ["default"] + } + ] +} +---- + +2. Check each saved object for `spacesWithMatchingOrigins` conflicts. ++ +Objects should not be shared to spaces with matching origins or you will create URL conflicts (causing the same URL to point to different saved objects). + +3. Check each saved object for `spacesWithMatchingAliases` conflicts. ++ +If these match the spaces that these saved objects will be shared to you should disable legacy URL aliases for them using the disable legacy URL aliases API. ++ +When sharing to all spaces (`*`) all entries in `spacesWithMatchingAliases` should be checked. + +4. Update spaces of each saved object and all its references: ++ +[source,sh] +---- +$ curl -X POST /api/spaces/_update_objects_spaces +{ + "objects": [ + { + "type": "index-pattern", + "id": "90943e30-9a47-11e8-b64d-95841ca0b247" + } + ], + "spacesToAdd": ["test"], + "spacesToRemove": [] +} +---- ++ +The API returns the following: ++ +[source,json] +---- +{ + "objects": [ + { + "type": "index-pattern", + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "spaces": ["default", "test"] + } + ] +} +---- diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4088342a6832b..1fa61881eca92 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -1147,3 +1147,53 @@ Refer to {api-kibana}/group/endpoint-cases[cases API]. == Sync {ml} saved objects API Refer to {api-kibana}/group/endpoint-ml[machine learning APIs]. + +[role="exclude",id="spaces-api-post"] +== Create space API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-put"] +== Update space API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-get"] +== Get a space API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-get-all"] +== Get all {kib} spaces API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-delete"] +== Delete space API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-copy-saved-objects"] +== Copy saved objects to space API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-disable-legacy-url-aliases"] +== Disable legacy URL aliases API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-get-shareable-references"] +== Get shareable references API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-resolve-copy-saved-objects-conflicts"] +== Resolve copy saved objects to space conflicts API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. + +[role="exclude",id="spaces-api-update-objects-spaces"] +== Update saved objects spaces API + +Refer to {api-kibana}/group/endpoint-spaces[spaces APIs]. \ No newline at end of file diff --git a/docs/upgrade-notes.asciidoc b/docs/upgrade-notes.asciidoc index 57b97856a3d40..43fb4d5ac66e5 100644 --- a/docs/upgrade-notes.asciidoc +++ b/docs/upgrade-notes.asciidoc @@ -1481,6 +1481,18 @@ The following rule action variables have been deprecated. Use the recommended va For more information, refer to ({kibana-pull}161136[#161136]). ==== +// Discover + +[discrete] +[[deprecation-search-sessions]] +.[Discover] <> are deprecated in 8.15.0 and will be removed in a future version. (8.15) +[%collapsible] +==== +*Details* + +Search sessions are now deprecated and will be removed in a future version. By default, queries that take longer than 10 minutes (the default for the advanced setting `search:timeout`) will be canceled. To allow queries to run longer, consider increasing `search:timeout` or setting it to `0` which will allow queries to continue running as long as a user is waiting on-screen for results. +==== + + // General settings [discrete] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 0365d39c43d3f..c46786b98829d 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -197,6 +197,8 @@ include::{kibana-root}/docs/management/rollups/create_and_manage_rollups.asciido include::{kibana-root}/docs/management/managing-saved-objects.asciidoc[] +include::{kibana-root}/docs/management/saved-objects/saved-object-ids.asciidoc[] + include::security/index.asciidoc[] include::{kibana-root}/docs/spaces/index.asciidoc[] diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 1757e0f4160d7..b1665860e42aa 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -7100,7 +7100,7 @@ }, "/api/spaces/_copy_saved_objects": { "post": { - "description": "Copy saved objects to spaces", + "description": "It also allows you to automatically copy related objects, so when you copy a dashboard, this can automatically copy over the associated visualizations, data views, and saved searches, as required. You can request to overwrite any objects that already exist in the target space if they share an identifier or you can use the resolve copy saved objects conflicts API to do this on a per-object basis.", "operationId": "%2Fapi%2Fspaces%2F_copy_saved_objects#0", "parameters": [ { @@ -7134,14 +7134,17 @@ "properties": { "compatibilityMode": { "default": false, + "description": "Apply various adjustments to the saved objects that are being copied to maintain compatibility between different Kibana versions. Use this option only if you encounter issues with copied saved objects. This option cannot be used with the `createNewCopies` option.", "type": "boolean" }, "createNewCopies": { "default": true, + "description": "Create new copies of saved objects, regenerate each object identifier, and reset the origin. When used, potential conflict errors are avoided. This option cannot be used with the `overwrite` and `compatibilityMode` options.", "type": "boolean" }, "includeReferences": { "default": false, + "description": "When set to true, all saved objects related to the specified saved objects will also be copied into the target spaces.", "type": "boolean" }, "objects": { @@ -7149,9 +7152,11 @@ "additionalProperties": false, "properties": { "id": { + "description": "The identifier of the saved object to copy.", "type": "string" }, "type": { + "description": "The type of the saved object to copy.", "type": "string" } }, @@ -7165,10 +7170,12 @@ }, "overwrite": { "default": false, + "description": "When set to true, all conflicts are automatically overridden. When a saved object with a matching type and identifier exists in the target space, that version is replaced with the version from the source space. This option cannot be used with the `createNewCopies` option.", "type": "boolean" }, "spaces": { "items": { + "description": "The identifiers of the spaces where you want to copy the specified objects.", "type": "string" }, "type": "array" @@ -7184,13 +7191,14 @@ } }, "responses": {}, - "summary": "", - "tags": [] + "summary": "Copy saved objects between spaces", + "tags": [ + "spaces" + ] } }, "/api/spaces/_disable_legacy_url_aliases": { "post": { - "description": "Disable legacy URL aliases", "operationId": "%2Fapi%2Fspaces%2F_disable_legacy_url_aliases#0", "parameters": [ { @@ -7227,12 +7235,15 @@ "additionalProperties": false, "properties": { "sourceId": { + "description": "The alias source object identifier. This is the legacy object identifier.", "type": "string" }, "targetSpace": { + "description": "The space where the alias target object exists.", "type": "string" }, "targetType": { + "description": "The type of alias target object. ", "type": "string" } }, @@ -7255,13 +7266,15 @@ } }, "responses": {}, - "summary": "", - "tags": [] + "summary": "Disable legacy URL aliases", + "tags": [ + "spaces" + ] } }, "/api/spaces/_get_shareable_references": { "post": { - "description": "Get shareable references", + "description": "Collect references and space contexts for saved objects.", "operationId": "%2Fapi%2Fspaces%2F_get_shareable_references#0", "parameters": [ { @@ -7322,13 +7335,15 @@ } }, "responses": {}, - "summary": "", - "tags": [] + "summary": "Get shareable references", + "tags": [ + "spaces" + ] } }, "/api/spaces/_resolve_copy_saved_objects_errors": { "post": { - "description": "Resolve conflicts copying saved objects", + "description": "Overwrite saved objects that are returned as errors from the copy saved objects to space API.", "operationId": "%2Fapi%2Fspaces%2F_resolve_copy_saved_objects_errors#0", "parameters": [ { @@ -7397,22 +7412,28 @@ "additionalProperties": false, "properties": { "createNewCopy": { + "description": "Creates new copies of the saved objects, regenerates each object ID, and resets the origin.", "type": "boolean" }, "destinationId": { + "description": "Specifies the destination identifier that the copied object should have, if different from the current identifier.", "type": "string" }, "id": { + "description": "The saved object identifier.", "type": "string" }, "ignoreMissingReferences": { + "description": "When set to true, any missing references errors are ignored.", "type": "boolean" }, "overwrite": { "default": false, + "description": "When set to true, the saved object from the source space overwrites the conflicting object in the destination space.", "type": "boolean" }, "type": { + "description": "The saved object type.", "type": "string" } }, @@ -7437,13 +7458,13 @@ } }, "responses": {}, - "summary": "", + "summary": "Resolve conflicts copying saved objects", "tags": [] } }, "/api/spaces/_update_objects_spaces": { "post": { - "description": "Update saved objects in spaces", + "description": "Update one or more saved objects to add or remove them from some spaces.", "operationId": "%2Fapi%2Fspaces%2F_update_objects_spaces#0", "parameters": [ { @@ -7480,9 +7501,11 @@ "additionalProperties": false, "properties": { "id": { + "description": "The identifier of the saved object to update.", "type": "string" }, "type": { + "description": "The type of the saved object to update.", "type": "string" } }, @@ -7496,12 +7519,14 @@ }, "spacesToAdd": { "items": { + "description": "The identifiers of the spaces the saved objects should be added to or removed from.", "type": "string" }, "type": "array" }, "spacesToRemove": { "items": { + "description": "The identifiers of the spaces the saved objects should be added to or removed from.", "type": "string" }, "type": "array" @@ -7518,13 +7543,14 @@ } }, "responses": {}, - "summary": "", - "tags": [] + "summary": "Update saved objects in spaces", + "tags": [ + "spaces" + ] } }, "/api/spaces/space": { "get": { - "description": "Get all spaces", "operationId": "%2Fapi%2Fspaces%2Fspace#0", "parameters": [ { @@ -7540,6 +7566,7 @@ } }, { + "description": "Specifies which authorization checks are applied to the API call. The default value is `any`.", "in": "query", "name": "purpose", "required": false, @@ -7553,6 +7580,7 @@ } }, { + "description": "When enabled, the API returns any spaces that the user is authorized to access in any capacity and each space will contain the purposes for which the user is authorized. This can be useful to determine which spaces a user can read but not take a specific action in. If the security plugin is not enabled, this parameter has no effect, since no authorization checks take place. This parameter cannot be used in with the `purpose` parameter.", "in": "query", "name": "include_authorized_purposes", "required": true, @@ -7592,14 +7620,17 @@ } } ], - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Get all spaces", "tags": [ "spaces" ] }, "post": { - "description": "Create a space", "operationId": "%2Fapi%2Fspaces%2Fspace#1", "parameters": [ { @@ -7635,29 +7666,36 @@ "type": "boolean" }, "color": { + "description": "The hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name.", "type": "string" }, "description": { + "description": "A description for the space.", "type": "string" }, "disabledFeatures": { "default": [], "items": { + "description": "The list of features that are turned off in the space.", "type": "string" }, "type": "array" }, "id": { + "description": "The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, _, and -). You are cannot change the ID with the update operation.", "type": "string" }, "imageUrl": { + "description": "The data-URL encoded image to display in the space avatar. If specified, initials will not be displayed and the color will be visible as the background color for transparent images. For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images.", "type": "string" }, "initials": { + "description": "One or two characters that are shown in the space avatar. By default, the initials are automatically generated from the space name.", "maxLength": 2, "type": "string" }, "name": { + "description": "The display name for the space. ", "minLength": 1, "type": "string" }, @@ -7680,8 +7718,12 @@ } } }, - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Create a space", "tags": [ "spaces" ] @@ -7689,7 +7731,7 @@ }, "/api/spaces/space/{id}": { "delete": { - "description": "Delete a space", + "description": "When you delete a space, all saved objects that belong to the space are automatically deleted, which is permanent and cannot be undone.", "operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2", "parameters": [ { @@ -7715,6 +7757,7 @@ } }, { + "description": "The space identifier.", "in": "path", "name": "id", "required": true, @@ -7723,14 +7766,20 @@ } } ], - "responses": {}, - "summary": "", + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "404": { + "description": "Indicates that the request failed." + } + }, + "summary": "Delete a space", "tags": [ "spaces" ] }, "get": { - "description": "Get a space", "operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0", "parameters": [ { @@ -7746,6 +7795,7 @@ } }, { + "description": "The space identifier.", "in": "path", "name": "id", "required": true, @@ -7754,14 +7804,17 @@ } } ], - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Get a space", "tags": [ "spaces" ] }, "put": { - "description": "Update a space", "operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1", "parameters": [ { @@ -7787,6 +7840,7 @@ } }, { + "description": "The space identifier. You are unable to change the ID with the update operation.", "in": "path", "name": "id", "required": true, @@ -7805,29 +7859,36 @@ "type": "boolean" }, "color": { + "description": "The hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name.", "type": "string" }, "description": { + "description": "A description for the space.", "type": "string" }, "disabledFeatures": { "default": [], "items": { + "description": "The list of features that are turned off in the space.", "type": "string" }, "type": "array" }, "id": { + "description": "The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, _, and -). You are cannot change the ID with the update operation.", "type": "string" }, "imageUrl": { + "description": "The data-URL encoded image to display in the space avatar. If specified, initials will not be displayed and the color will be visible as the background color for transparent images. For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images.", "type": "string" }, "initials": { + "description": "One or two characters that are shown in the space avatar. By default, the initials are automatically generated from the space name.", "maxLength": 2, "type": "string" }, "name": { + "description": "The display name for the space. ", "minLength": 1, "type": "string" }, @@ -7850,8 +7911,12 @@ } } }, - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Update a space", "tags": [ "spaces" ] diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index a3d09fdd76a02..3631a9fffb889 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -6409,7 +6409,6 @@ }, "/api/spaces/space": { "get": { - "description": "Get all spaces", "operationId": "%2Fapi%2Fspaces%2Fspace#0", "parameters": [ { @@ -6425,6 +6424,7 @@ } }, { + "description": "Specifies which authorization checks are applied to the API call. The default value is `any`.", "in": "query", "name": "purpose", "required": false, @@ -6438,6 +6438,7 @@ } }, { + "description": "When enabled, the API returns any spaces that the user is authorized to access in any capacity and each space will contain the purposes for which the user is authorized. This can be useful to determine which spaces a user can read but not take a specific action in. If the security plugin is not enabled, this parameter has no effect, since no authorization checks take place. This parameter cannot be used in with the `purpose` parameter.", "in": "query", "name": "include_authorized_purposes", "required": true, @@ -6477,14 +6478,17 @@ } } ], - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Get all spaces", "tags": [ "spaces" ] }, "post": { - "description": "Create a space", "operationId": "%2Fapi%2Fspaces%2Fspace#1", "parameters": [ { @@ -6520,29 +6524,36 @@ "type": "boolean" }, "color": { + "description": "The hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name.", "type": "string" }, "description": { + "description": "A description for the space.", "type": "string" }, "disabledFeatures": { "default": [], "items": { + "description": "The list of features that are turned off in the space.", "type": "string" }, "type": "array" }, "id": { + "description": "The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, _, and -). You are cannot change the ID with the update operation.", "type": "string" }, "imageUrl": { + "description": "The data-URL encoded image to display in the space avatar. If specified, initials will not be displayed and the color will be visible as the background color for transparent images. For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images.", "type": "string" }, "initials": { + "description": "One or two characters that are shown in the space avatar. By default, the initials are automatically generated from the space name.", "maxLength": 2, "type": "string" }, "name": { + "description": "The display name for the space. ", "minLength": 1, "type": "string" } @@ -6556,8 +6567,12 @@ } } }, - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Create a space", "tags": [ "spaces" ] @@ -6565,7 +6580,7 @@ }, "/api/spaces/space/{id}": { "delete": { - "description": "Delete a space", + "description": "When you delete a space, all saved objects that belong to the space are automatically deleted, which is permanent and cannot be undone.", "operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2", "parameters": [ { @@ -6591,6 +6606,7 @@ } }, { + "description": "The space identifier.", "in": "path", "name": "id", "required": true, @@ -6599,14 +6615,20 @@ } } ], - "responses": {}, - "summary": "", + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "404": { + "description": "Indicates that the request failed." + } + }, + "summary": "Delete a space", "tags": [ "spaces" ] }, "get": { - "description": "Get a space", "operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0", "parameters": [ { @@ -6622,6 +6644,7 @@ } }, { + "description": "The space identifier.", "in": "path", "name": "id", "required": true, @@ -6630,14 +6653,17 @@ } } ], - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Get a space", "tags": [ "spaces" ] }, "put": { - "description": "Update a space", "operationId": "%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1", "parameters": [ { @@ -6663,6 +6689,7 @@ } }, { + "description": "The space identifier. You are unable to change the ID with the update operation.", "in": "path", "name": "id", "required": true, @@ -6681,29 +6708,36 @@ "type": "boolean" }, "color": { + "description": "The hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name.", "type": "string" }, "description": { + "description": "A description for the space.", "type": "string" }, "disabledFeatures": { "default": [], "items": { + "description": "The list of features that are turned off in the space.", "type": "string" }, "type": "array" }, "id": { + "description": "The space ID that is part of the Kibana URL when inside the space. Space IDs are limited to lowercase alphanumeric, underscore, and hyphen characters (a-z, 0-9, _, and -). You are cannot change the ID with the update operation.", "type": "string" }, "imageUrl": { + "description": "The data-URL encoded image to display in the space avatar. If specified, initials will not be displayed and the color will be visible as the background color for transparent images. For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images.", "type": "string" }, "initials": { + "description": "One or two characters that are shown in the space avatar. By default, the initials are automatically generated from the space name.", "maxLength": 2, "type": "string" }, "name": { + "description": "The display name for the space. ", "minLength": 1, "type": "string" } @@ -6717,8 +6751,12 @@ } } }, - "responses": {}, - "summary": "", + "responses": { + "200": { + "description": "Indicates a successful call." + } + }, + "summary": "Update a space", "tags": [ "spaces" ] diff --git a/oas_docs/examples/copy_saved_objects_request1.yaml b/oas_docs/examples/copy_saved_objects_request1.yaml new file mode 100644 index 0000000000000..4ddb9cc39da22 --- /dev/null +++ b/oas_docs/examples/copy_saved_objects_request1.yaml @@ -0,0 +1,11 @@ +summary: Copy with createNewCopies +description: > + Copy a dashboard with the my-dashboard ID, including all references from the default space to the marketing space. + In this example, the dashboard has a reference to a visualization and that has a reference to a data view. +value: + objects: + - type: dashboard + id: my-dashboard + spaces: + - marketing + includeReferences: true \ No newline at end of file diff --git a/oas_docs/examples/copy_saved_objects_request2.yaml b/oas_docs/examples/copy_saved_objects_request2.yaml new file mode 100644 index 0000000000000..52ae83b66b95d --- /dev/null +++ b/oas_docs/examples/copy_saved_objects_request2.yaml @@ -0,0 +1,12 @@ +summary: Copy without createNewCopies +description: > + Copy a dashboard with the my-dashboard ID, including all references from the default space to the marketing space. + In this example, the dashboard has a reference to a visualization and that has a reference to a data view. +value: + objects: + - type: dashboard + id: my-dashboard + spaces: + - marketing + includeReferences: true + createNewCopies: false \ No newline at end of file diff --git a/oas_docs/examples/copy_saved_objects_response1.yaml b/oas_docs/examples/copy_saved_objects_response1.yaml new file mode 100644 index 0000000000000..d65fc50c5a317 --- /dev/null +++ b/oas_docs/examples/copy_saved_objects_response1.yaml @@ -0,0 +1,28 @@ +summary: Copy with createNewCopies +description: > + The response for successfully copying a dashboard with the my-dashboard ID, including all references from the default space to the marketing space. + The result indicates a successful copy and all three objects are created. + Since these objects were created as new copies, each entry in the successResults array includes a destinationId attribute. +value: + marketing: + success: true + successCount: 3 + successResults: + - id: my-dashboard + type: dashboard + destinationId: "1e127098-5b80-417f-b0f1-c60c8395358f" + meta: + icon: dashboardApp + title: Look at my dashboard + - id: my-vis + type: visualization + destinationId: "a610ed80-1c73-4507-9e13-d3af736c8e04" + meta: + icon: visualizeApp + title: Look at my visualization + - id: my-index-pattern + type: index-pattern + destinationId: "bc3c9c70-bf6f-4bec-b4ce-f4189aa9e26b" + meta: + icon: indexPatternApp + title: my-pattern-* diff --git a/oas_docs/examples/copy_saved_objects_response2.yaml b/oas_docs/examples/copy_saved_objects_response2.yaml new file mode 100644 index 0000000000000..1e49191eaeaa1 --- /dev/null +++ b/oas_docs/examples/copy_saved_objects_response2.yaml @@ -0,0 +1,24 @@ +summary: Copy without createNewCopies +description: > + The response for successfully copying a dashboard with the my-dashboard ID with createNewCopies turned off. + The result indicates a successful copy and all three objects are created. +value: + marketing: + success: true + successCount: 3 + successResults: + - id: my-dashboard + type: dashboard + meta: + icon: dashboardApp + title: Look at my dashboard + - id: my-vis + type: visualization + meta: + icon: visualizeApp + title: Look at my visualization + - id: my-index-pattern + type: index-pattern + meta: + icon: indexPatternApp + title: my-pattern-* diff --git a/oas_docs/examples/copy_saved_objects_response3.yaml b/oas_docs/examples/copy_saved_objects_response3.yaml new file mode 100644 index 0000000000000..aa497ca6bc520 --- /dev/null +++ b/oas_docs/examples/copy_saved_objects_response3.yaml @@ -0,0 +1,72 @@ +summary: Failed copy response with conflict errors +description: > + A response for a failed copy of a dashboard with the my-dashboard ID including all references from the default space to the marketing and sales spaces. In this example, the dashboard has a reference to a visualization and a Canvas workpad and the visualization has a reference to an index pattern. + The result indicates a successful copy for the marketing space and an unsuccessful copy for the sales space because the data view, visualization, and Canvas workpad each resulted in a conflict error. + Objects are created when the error is resolved using the resolve copy conflicts API. +value: + marketing: + success: true + successCount: 4 + successResults: + - id: my-dashboard + type: dashboard + meta: + icon: dashboardApp + title: Look at my dashboard + - id: my-vis + type: visualization + meta: + icon: visualizeApp + title: Look at my visualization + - id: my-canvas + type: canvas-workpad + meta: + icon: canvasApp + title: Look at my canvas + - id: my-index-pattern + type: index-pattern + meta: + icon: indexPatternApp + title: my-pattern-* + sales: + success: false + successCount: 1, + errors: + - id: my-pattern + type: index-pattern + title: my-pattern-* + error: + type: conflict + meta: + icon: indexPatternApp + title: my-pattern-* + - id: my-visualization + type: my-vis + title: Look at my visualization + error: + type: conflict + destinationId: another-vis + meta: + icon: visualizeApp + title: Look at my visualization + - id: my-canvas + type: canvas-workpad + title: Look at my canvas + error: + type: ambiguous_conflict + destinations: + - id: another-canvas + title: Look at another canvas + updatedAt: "2020-07-08T16:36:32.377Z" + - id: yet-another-canvas + title: Look at yet another canvas + updatedAt: "2020-07-05T12:29:54.849Z" + meta: + icon: canvasApp + title: Look at my canvas + successResults": + - id: my-dashboard + type: dashboard + meta: + icon: dashboardApp + title: Look at my dashboard \ No newline at end of file diff --git a/oas_docs/examples/copy_saved_objects_response4.yaml b/oas_docs/examples/copy_saved_objects_response4.yaml new file mode 100644 index 0000000000000..c9acefafb1d6e --- /dev/null +++ b/oas_docs/examples/copy_saved_objects_response4.yaml @@ -0,0 +1,33 @@ +summary: Failed copy with missing reference errors +description: > + The response for successfully copying a dashboard with the my-dashboard ID, including all references from the default space to the marketing space. + In this example, the dashboard has a reference to a visualization and a Canvas workpad and the visualization has a reference to a data view. + The result indicates an unsuccessful copy because the visualization resulted in a missing references error. + Objects are created when the errors are resolved using the resolve copy conflicts API. +value: + marketing: + success: false + successCount: 2 + errors: + - id: my-vis + type: visualization + title: Look at my visualization + error: + type: missing_references + references: + - type: index-pattern + id: my-pattern-* + meta: + icon: visualizeApp + title: Look at my visualization + successResults: + - id: my-dashboard + type: dashboard + meta: + icon: dashboardApp + title: Look at my dashboard + - id: my-canvas + type: canvas-workpad + meta: + icon: canvasApp + title: Look at my canvas diff --git a/oas_docs/examples/create_space_request.yaml b/oas_docs/examples/create_space_request.yaml new file mode 100644 index 0000000000000..0e037504f7e7c --- /dev/null +++ b/oas_docs/examples/create_space_request.yaml @@ -0,0 +1,9 @@ +summary: Create a marketing space +value: + id: marketing + name: Marketing + description : This is the Marketing Space + color: #aabbcc + initials: MK + disabledFeatures: [] + imageUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABACAYAAABC6cT1AAAGf0lEQVRoQ+3abYydRRUH8N882xYo0IqagEVjokQJKAiKBjXExC9G/aCkGowCIghCkRcrVSSKIu/FEiqgGL6gBIlAYrAqUTH6hZgQFVEMKlQFfItWoQWhZe8z5uzMLdvbfbkLxb13d+fbvfe588x/zpn/+Z9zJpmnI81T3BaAzzfLL1h8weLzZAcWXH2eGHo7zAWLL1h8nuzAjFw9G1N6Kzq8HnuM36MR8iibF3Fv4q+7cv8yDV6K13bYq2furSP8Ag8ncr/vnSnwRViJT2GfCV7yL1yHGxLb+l3EdM9lluNEnIC9xz+f2ZL4Er6Z2DrdXN3fZwp8CU7OfDHxggle8lTLbQ1nJ/7Z7yKmey5zYGZt4h2IzR8/trRc2PDlxJPTzfVcgJ+CC0wMPOa9F6cm7up3EVM9V9386MxliVdM8GwAv6hh/awCz/w7lY25OtF5ruBz4ZLP42NYNrDAFbC3YPWuILnMAfgq3oaRQQYea/stViV+sgssvjKzLvGySeaaNVfP4d7Btokgvxj/bblgpueuF1hmWcyTCmfE3J3M1lTcv0vMswM88zR+jpw4osu6me8kzkpsfLZWzxyRuabO22buxxOJ12FxnXfWgEe83pB5sOE47BsLymzscOoi7nw2JJfZreUjiUsTyzKPZm5NvBDvSuw268AzNzV8H5/Am+qCnsAXgpgSW2Zq9cyKlksbPlTd+te4quWNieMHBfiNDdciYnwsdI/MaOaWhnMTf54J8CqNj8x8JXFIZltYu+HqlmNT8YSBsHgAPw/vxvlVV4du/s0oaxbxg0TbL/jMni0nNcVjQq7+HZfgtpbzBg342TgQ63AkmsymxBW4IjE6A+D7Vzd/fyWxIM/VuCe+HzTgZ2Jpy/kNJ2FJLmLm24mPJ/42A+Bvrxt4SISwlhsaPodH26LZB8rVA3inwwebsrixJCZzX+KMxI/7AV61eVh3DV6Mx3EOvh4kN6jAg8nfUCXm4d1wE66OyxNPTQc+s3/o/MoXizL3JE5O3F3P/uBZPPF4Zr+Wi5uSO48ZPRdyCwn7YB/A35m5KhWNHox4fcNnIs0ddOCRSBxf8+cQG+Huf0l8NJVYP+nI7NXy2ar4QqIGm69JfKPOE2w/mBavCzwM11R2D+ChsUO7hyUfmwx55qDM1xJvqZ7y08TpifuGBfjeURVJnNIVGpkNiXNS0ds7jcySDitDCCWW56LJ10fRo8sNA+3qXUSZD2CtQlZh9T+1rB7h9oliembflnMbzqgSNZKbKGHdPm7OwXb1CvQ1metSETMpszmzvikCJNh/h5E5PHNl4qga/+/cxqrdeWDYgIe7X5L4cGJPJX2940lOX8pD41FnFnc4riluvQKbK0dcHJFi2IBHNTQSlguru4d2/wPOTNzRA3x5y+U1E1uqWDkETOT026XuUJzx6u7ReLhSYenQ7uHua0fKZmwfmcPqsQjxE5WVONcRxn7X89zgn/EKPMRMxOVQXmP18Mx3q3b/Y/0cQE/IhFtHESMsHFlZ1Ml3CH3DZPHImY+pxcKumNmYirtvqMBfhMuU6s3iqOQkTsMPe1tCQwO8Ajs0lxr7W+vnp1MJc9EgCNd/cy6x+9D4veXmprj5wxMw/3C4egW6zzgZOlYZzfwo3F2J7ael0pJamvlPKgWNKFft1AAcKotXoFEbD7kaoSoQPVKB35+5KHF0lai/rJo+up87jWEE/qqqwY+qrL21LWLm95lPJ16ppKw31XC3PXYPJauPEx7B6BHCgrSizRs18qiaRp8tlN3ueCTYPHH9RNaunjI8Z7wLYpT3jZSCYXQ8e9vTsRE/q+no3XMKeObgGtaintbb/AvXj4JDkNw/5hrwYPfIvlZFUbLn7G5q+eQIN09Vnho6cqvnM/Lt99RixH49wO8K0ZL41WTWHoQzvsNVkOheZqKhEGpsp3SzB+BBtZAYve7uOR9tuTaaB6l0XScdYfEQPpkTUyHEGP+XqyDBzu+NBCITUjNWHynkrbWKOuWFn1xKzqsyx0bdvS78odp0+N503Zao0uCsWuSIDku8/7EO60b41vN5+Ses9BKlTdvd8bhp9EBvJjWJAIn/vxwHe6b3tSk6JFPV4nq85oAOrx555v/x/rh3E6Lo+bnuNS4uB4Cuq0ZfvO8X1rM6q/+vnjLVqZq7v83onttc2oYF4HPJmv1gWbB4P7s0l55ZsPhcsmY/WBYs3s8uzaVn5q3F/wf70mRuBCtbjQAAAABJRU5ErkJggg==" \ No newline at end of file diff --git a/oas_docs/examples/disable_legacy_url_request1.yaml b/oas_docs/examples/disable_legacy_url_request1.yaml new file mode 100644 index 0000000000000..a85a14d4e1c16 --- /dev/null +++ b/oas_docs/examples/disable_legacy_url_request1.yaml @@ -0,0 +1,9 @@ +summary: Disable legacy URL aliases +description: > + This request leaves the alias intact but the legacy URL for this alias (http://localhost:5601/s/bills-space/app/dashboards#/view/123) will no longer function. + The dashboard still exists and you can access it with the new URL. +value: + aliases: + - targetSpace: bills-space + targetType: dashboard + sourceId: 123 \ No newline at end of file diff --git a/oas_docs/examples/get_space_response.yaml b/oas_docs/examples/get_space_response.yaml new file mode 100644 index 0000000000000..6cf8a250d90aa --- /dev/null +++ b/oas_docs/examples/get_space_response.yaml @@ -0,0 +1,10 @@ +summary: Get details about a marketing space +value: + id: marketing + name: Marketing + description : This is the Marketing Space + color: #aabbcc + initials: MK + disabledFeatures: [] + imageUrl: "" + solution: es \ No newline at end of file diff --git a/oas_docs/examples/get_spaces_response1.yaml b/oas_docs/examples/get_spaces_response1.yaml new file mode 100644 index 0000000000000..88e938821a378 --- /dev/null +++ b/oas_docs/examples/get_spaces_response1.yaml @@ -0,0 +1,24 @@ +summary: Get all spaces +description: Get all spaces without specifying any options. +value: + - id: default + name: Default + description: This is the Default Space + disabledFeatures: [] + imageUrl: "" + _reserved: true + - id: marketing + name: Marketing + description: This is the Marketing Space + color: #aabbcc + disabledFeatures: + - apm + initials: MK + imageUrl: "data:image/png;base64,iVBORw0KGgoAAAANSU" + - id: sales + name: Sales + initials: MK + disabledFeatures: + - discover + imageUr": "" + solution: oblt \ No newline at end of file diff --git a/oas_docs/examples/get_spaces_response2.yaml b/oas_docs/examples/get_spaces_response2.yaml new file mode 100644 index 0000000000000..a95c92676b236 --- /dev/null +++ b/oas_docs/examples/get_spaces_response2.yaml @@ -0,0 +1,41 @@ +summary: Get all spaces with custom options +description: > + The user has read-only access to the Sales space. + Get all spaces with the following query parameters: + "purpose=shareSavedObjectsIntoSpace&include_authorized_purposes=true" +value: + - id: default + name: Default + description: This is the Default Space + disabledFeatures: [] + imageUrl: "" + _reserved: true + authorizedPurposes: + any: true + copySavedObjectsIntoSpace: true + findSavedObjects: true + shareSavedObjectsIntoSpace: true + - id: marketing + name: Marketing + description: This is the Marketing Space + color: #aabbcc + disabledFeatures: + - apm + initials: MK + imageUrl: "data:image/png;base64,iVBORw0KGgoAAAANSU" + authorizedPurposes: + any: true + copySavedObjectsIntoSpace: true + findSavedObjects: true + shareSavedObjectsIntoSpace: true + - id: sales + name: Sales + initials: MK + disabledFeatures: + - discover + imageUrl: "" + authorizedPurposes: + any: true + copySavedObjectsIntoSpace: false + findSavedObjects: true + shareSavedObjectsIntoSpace: false \ No newline at end of file diff --git a/oas_docs/examples/resolve_copy_saved_objects_request1.yaml b/oas_docs/examples/resolve_copy_saved_objects_request1.yaml new file mode 100644 index 0000000000000..11cee7931e38c --- /dev/null +++ b/oas_docs/examples/resolve_copy_saved_objects_request1.yaml @@ -0,0 +1,26 @@ +summary: Resolve conflict errors +description: > + Resolve conflict errors for a data view, visualization, and Canvas workpad by overwriting the existing saved objects. + NOTE: If a prior copy attempt resulted in resolvable errors, you must include a retry for each object you want to copy, including any that were returned in the successResults array. + In this example, we retried copying the dashboard accordingly. +value: + objects: + - type: dashboard + id: my-dashboard + includeReferences: true + createNewCopies: false + retries: + sales: + - type: index-pattern + id: my-pattern + overwrite: true + - type: visualization + id: my-vis + overwrite: true, + destinationId: another-vis + - type: canvas + id: my-canvas + overwrite: true + destinationId: yet-another-canvas + - type: dashboard + id: my-dashboard \ No newline at end of file diff --git a/oas_docs/examples/resolve_copy_saved_objects_request2.yaml b/oas_docs/examples/resolve_copy_saved_objects_request2.yaml new file mode 100644 index 0000000000000..e8438b77fc1a5 --- /dev/null +++ b/oas_docs/examples/resolve_copy_saved_objects_request2.yaml @@ -0,0 +1,20 @@ +summary: Resolve missing reference errors +description: > + Resolve missing reference errors for a visualization by ignoring the error. + NOTE: If a prior copy attempt resulted in resolvable errors, you must include a retry for each object you want to copy, including any that were returned in the successResults array. + In this example, we retried copying the dashboard and canvas accordingly. +value: + objects: + - type: dashboard + id: my-dashboard + includeReferences: true + createNewCopies: false + retries: + marketing: + - type: visualization + id: my-vis + ignoreMissingReferences: true + - type: canvas + id: my-canvas + - type: dashboard + id: my-dashboard diff --git a/oas_docs/examples/resolve_copy_saved_objects_response1.yaml b/oas_docs/examples/resolve_copy_saved_objects_response1.yaml new file mode 100644 index 0000000000000..fee42792f05a0 --- /dev/null +++ b/oas_docs/examples/resolve_copy_saved_objects_response1.yaml @@ -0,0 +1,31 @@ +summary: Resolve conflict errors +description: > + The response for resolving conflict errors for a data view, visualization, and Canvas workpad by overwriting the existing saved objects. + The result indicates a successful copy and all four objects are created. +value: + sales: + success: true + successCount: 4 + successResults: + - id: my-pattern + type: index-pattern + meta: + icon: indexPatternApp + title: my-pattern-* + - id: my-vis + type: visualization + destinationId: another-vis + meta: + icon: visualizeApp + title: Look at my visualization + - id: my-canvas + type: canvas-workpad + destinationId: yet-another-canvas + meta: + icon: canvasApp + title: Look at my canvas + - id: my-dashboard + type: dashboard + meta: + icon: dashboardApp + title: Look at my dashboard diff --git a/oas_docs/examples/resolve_copy_saved_objects_response2.yaml b/oas_docs/examples/resolve_copy_saved_objects_response2.yaml new file mode 100644 index 0000000000000..0ea8f58ce405d --- /dev/null +++ b/oas_docs/examples/resolve_copy_saved_objects_response2.yaml @@ -0,0 +1,24 @@ +summary: Resolve conflict errors +description: > + The response for resolving missing reference errors for a visualization. + The result indicates a successful copy and all three objects are created. +value: + marketing: + success: true + successCount: 3 + successResults: + - id: my-vis + type: visualization + meta: + icon: visualizeApp + title: Look at my visualization + - id: my-canvas + type: canvas-workpad + meta: + icon: canvasApp + title: Look at my canvas + - id: my-dashboard + type: dashboard + meta: + icon: dashboardApp + title: Look at my dashboard diff --git a/oas_docs/examples/update_saved_objects_spaces_request1.yaml b/oas_docs/examples/update_saved_objects_spaces_request1.yaml new file mode 100644 index 0000000000000..0f341e6c3ace4 --- /dev/null +++ b/oas_docs/examples/update_saved_objects_spaces_request1.yaml @@ -0,0 +1,9 @@ +summary: Update saved object spaces +description: Update the spaces of each saved object and all its references. +value: + objects: + - type: index-pattern + id: 90943e30-9a47-11e8-b64d-95841ca0b247 + spacesToAdd: + - test + spacesToRemove: [] \ No newline at end of file diff --git a/oas_docs/examples/update_saved_objects_spaces_response1.yaml b/oas_docs/examples/update_saved_objects_spaces_response1.yaml new file mode 100644 index 0000000000000..1f1c1d47c8b06 --- /dev/null +++ b/oas_docs/examples/update_saved_objects_spaces_response1.yaml @@ -0,0 +1,10 @@ +summary: Update saved object spaces +description: > + The response from updating the spaces of saved objects. +value: + objects: + - type: index-pattern + id: 90943e30-9a47-11e8-b64d-95841ca0b247 + spaces: + - default + - test \ No newline at end of file diff --git a/oas_docs/examples/update_space_request.yaml b/oas_docs/examples/update_space_request.yaml new file mode 100644 index 0000000000000..42d1972390759 --- /dev/null +++ b/oas_docs/examples/update_space_request.yaml @@ -0,0 +1,10 @@ +summary: Update a marketing space +description: Update the marketing space to remove the imageUrl. +value: + id: marketing + name: Marketing + description : This is the Marketing Space + color: #aabbcc + initials: MK + disabledFeatures: [] + imageUrl: "" \ No newline at end of file diff --git a/oas_docs/makefile b/oas_docs/makefile index 717895f9b6c1c..ce24aa5b7b8b7 100644 --- a/oas_docs/makefile +++ b/oas_docs/makefile @@ -52,11 +52,13 @@ api-docs-overlay: ## Run spectral API docs linter on kibana.serverless.yaml @npx bump overlay "output/kibana.serverless.yaml" "overlays/kibana.overlays.serverless.yaml" > "output/kibana.serverless.tmp1.yaml" @npx bump overlay "output/kibana.serverless.tmp1.yaml" "overlays/alerting.overlays.yaml" > "output/kibana.serverless.tmp2.yaml" @npx bump overlay "output/kibana.serverless.tmp2.yaml" "overlays/connectors.overlays.yaml" > "output/kibana.serverless.tmp3.yaml" + @npx bump overlay "output/kibana.serverless.tmp3.yaml" "overlays/kibana.overlays.shared.yaml" > "output/kibana.serverless.tmp4.yaml" @npx bump overlay "output/kibana.yaml" "overlays/kibana.overlays.yaml" > "output/kibana.tmp1.yaml" @npx bump overlay "output/kibana.tmp1.yaml" "overlays/alerting.overlays.yaml" > "output/kibana.tmp2.yaml" @npx bump overlay "output/kibana.tmp2.yaml" "overlays/connectors.overlays.yaml" > "output/kibana.tmp3.yaml" - @npx @redocly/cli bundle output/kibana.serverless.tmp3.yaml --ext yaml -o output/kibana.serverless.new.yaml - @npx @redocly/cli bundle output/kibana.tmp3.yaml --ext yaml -o output/kibana.new.yaml + @npx bump overlay "output/kibana.tmp3.yaml" "overlays/kibana.overlays.shared.yaml" > "output/kibana.tmp4.yaml" + @npx @redocly/cli bundle output/kibana.serverless.tmp4.yaml --ext yaml -o output/kibana.serverless.new.yaml + @npx @redocly/cli bundle output/kibana.tmp4.yaml --ext yaml -o output/kibana.new.yaml rm output/kibana.tmp*.yaml rm output/kibana.serverless.tmp*.yaml diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index 2634ca5ad5959..7423338ee66aa 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -16302,7 +16302,6 @@ paths: - Prompts API /api/spaces/space: get: - description: Get all spaces operationId: '%2Fapi%2Fspaces%2Fspace#0' parameters: - description: The version of the API to use @@ -16313,7 +16312,10 @@ paths: enum: - '2023-10-31' type: string - - in: query + - description: >- + Specifies which authorization checks are applied to the API call. + The default value is `any`. + in: query name: purpose required: false schema: @@ -16322,7 +16324,15 @@ paths: - copySavedObjectsIntoSpace - shareSavedObjectsIntoSpace type: string - - in: query + - description: >- + When enabled, the API returns any spaces that the user is authorized + to access in any capacity and each space will contain the purposes + for which the user is authorized. This can be useful to determine + which spaces a user can read but not take a specific action in. If + the security plugin is not enabled, this parameter has no effect, + since no authorization checks take place. This parameter cannot be + used in with the `purpose` parameter. + in: query name: include_authorized_purposes required: true schema: @@ -16341,12 +16351,13 @@ paths: x-oas-optional: true - type: boolean x-oas-optional: true - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get all spaces tags: - spaces post: - description: Create a space operationId: '%2Fapi%2Fspaces%2Fspace#1' parameters: - description: The version of the API to use @@ -16374,34 +16385,61 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Create a space tags: - spaces '/api/spaces/space/{id}': delete: - description: Delete a space + description: >- + When you delete a space, all saved objects that belong to the space are + automatically deleted, which is permanent and cannot be undone. operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2' parameters: - description: The version of the API to use @@ -16419,17 +16457,21 @@ paths: schema: example: 'true' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '204': + description: Indicates a successful call. + '404': + description: Indicates that the request failed. + summary: Delete a space tags: - spaces get: - description: Get a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0' parameters: - description: The version of the API to use @@ -16440,17 +16482,19 @@ paths: enum: - '2023-10-31' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get a space tags: - spaces put: - description: Update a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1' parameters: - description: The version of the API to use @@ -16468,7 +16512,10 @@ paths: schema: example: 'true' type: string - - in: path + - description: >- + The space identifier. You are unable to change the ID with the + update operation. + in: path name: id required: true schema: @@ -16483,29 +16530,54 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Update a space tags: - spaces /api/status: @@ -31687,6 +31759,7 @@ components: Security_Timeline_API_AssociatedFilterType: description: Filter notes based on their association with a document or saved object. enum: + - all - document_only - saved_object_only - document_and_saved_object diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 2634ca5ad5959..7423338ee66aa 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -16302,7 +16302,6 @@ paths: - Prompts API /api/spaces/space: get: - description: Get all spaces operationId: '%2Fapi%2Fspaces%2Fspace#0' parameters: - description: The version of the API to use @@ -16313,7 +16312,10 @@ paths: enum: - '2023-10-31' type: string - - in: query + - description: >- + Specifies which authorization checks are applied to the API call. + The default value is `any`. + in: query name: purpose required: false schema: @@ -16322,7 +16324,15 @@ paths: - copySavedObjectsIntoSpace - shareSavedObjectsIntoSpace type: string - - in: query + - description: >- + When enabled, the API returns any spaces that the user is authorized + to access in any capacity and each space will contain the purposes + for which the user is authorized. This can be useful to determine + which spaces a user can read but not take a specific action in. If + the security plugin is not enabled, this parameter has no effect, + since no authorization checks take place. This parameter cannot be + used in with the `purpose` parameter. + in: query name: include_authorized_purposes required: true schema: @@ -16341,12 +16351,13 @@ paths: x-oas-optional: true - type: boolean x-oas-optional: true - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get all spaces tags: - spaces post: - description: Create a space operationId: '%2Fapi%2Fspaces%2Fspace#1' parameters: - description: The version of the API to use @@ -16374,34 +16385,61 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Create a space tags: - spaces '/api/spaces/space/{id}': delete: - description: Delete a space + description: >- + When you delete a space, all saved objects that belong to the space are + automatically deleted, which is permanent and cannot be undone. operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2' parameters: - description: The version of the API to use @@ -16419,17 +16457,21 @@ paths: schema: example: 'true' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '204': + description: Indicates a successful call. + '404': + description: Indicates that the request failed. + summary: Delete a space tags: - spaces get: - description: Get a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0' parameters: - description: The version of the API to use @@ -16440,17 +16482,19 @@ paths: enum: - '2023-10-31' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get a space tags: - spaces put: - description: Update a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1' parameters: - description: The version of the API to use @@ -16468,7 +16512,10 @@ paths: schema: example: 'true' type: string - - in: path + - description: >- + The space identifier. You are unable to change the ID with the + update operation. + in: path name: id required: true schema: @@ -16483,29 +16530,54 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Update a space tags: - spaces /api/status: @@ -31687,6 +31759,7 @@ components: Security_Timeline_API_AssociatedFilterType: description: Filter notes based on their association with a document or saved object. enum: + - all - document_only - saved_object_only - document_and_saved_object diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 647ad65e617e0..f7c5e34257336 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -20841,7 +20841,13 @@ paths: - roles /api/spaces/_copy_saved_objects: post: - description: Copy saved objects to spaces + description: >- + It also allows you to automatically copy related objects, so when you + copy a dashboard, this can automatically copy over the associated + visualizations, data views, and saved searches, as required. You can + request to overwrite any objects that already exist in the target space + if they share an identifier or you can use the resolve copy saved + objects conflicts API to do this on a per-object basis. operationId: '%2Fapi%2Fspaces%2F_copy_saved_objects#0' parameters: - description: The version of the API to use @@ -20868,12 +20874,26 @@ paths: properties: compatibilityMode: default: false + description: >- + Apply various adjustments to the saved objects that are + being copied to maintain compatibility between different + Kibana versions. Use this option only if you encounter + issues with copied saved objects. This option cannot be used + with the `createNewCopies` option. type: boolean createNewCopies: default: true + description: >- + Create new copies of saved objects, regenerate each object + identifier, and reset the origin. When used, potential + conflict errors are avoided. This option cannot be used + with the `overwrite` and `compatibilityMode` options. type: boolean includeReferences: default: false + description: >- + When set to true, all saved objects related to the specified + saved objects will also be copied into the target spaces. type: boolean objects: items: @@ -20881,8 +20901,10 @@ paths: type: object properties: id: + description: The identifier of the saved object to copy. type: string type: + description: The type of the saved object to copy. type: string required: - type @@ -20890,20 +20912,29 @@ paths: type: array overwrite: default: false + description: >- + When set to true, all conflicts are automatically + overridden. When a saved object with a matching type and + identifier exists in the target space, that version is + replaced with the version from the source space. This option + cannot be used with the `createNewCopies` option. type: boolean spaces: items: + description: >- + The identifiers of the spaces where you want to copy the + specified objects. type: string type: array required: - spaces - objects responses: {} - summary: '' - tags: [] + summary: Copy saved objects between spaces + tags: + - spaces /api/spaces/_disable_legacy_url_aliases: post: - description: Disable legacy URL aliases operationId: '%2Fapi%2Fspaces%2F_disable_legacy_url_aliases#0' parameters: - description: The version of the API to use @@ -20934,10 +20965,15 @@ paths: type: object properties: sourceId: + description: >- + The alias source object identifier. This is the legacy + object identifier. type: string targetSpace: + description: The space where the alias target object exists. type: string targetType: + description: 'The type of alias target object. ' type: string required: - targetSpace @@ -20947,11 +20983,12 @@ paths: required: - aliases responses: {} - summary: '' - tags: [] + summary: Disable legacy URL aliases + tags: + - spaces /api/spaces/_get_shareable_references: post: - description: Get shareable references + description: Collect references and space contexts for saved objects. operationId: '%2Fapi%2Fspaces%2F_get_shareable_references#0' parameters: - description: The version of the API to use @@ -20992,11 +21029,14 @@ paths: required: - objects responses: {} - summary: '' - tags: [] + summary: Get shareable references + tags: + - spaces /api/spaces/_resolve_copy_saved_objects_errors: post: - description: Resolve conflicts copying saved objects + description: >- + Overwrite saved objects that are returned as errors from the copy saved + objects to space API. operationId: '%2Fapi%2Fspaces%2F_resolve_copy_saved_objects_errors#0' parameters: - description: The version of the API to use @@ -21050,17 +21090,33 @@ paths: type: object properties: createNewCopy: + description: >- + Creates new copies of the saved objects, regenerates + each object ID, and resets the origin. type: boolean destinationId: + description: >- + Specifies the destination identifier that the copied + object should have, if different from the current + identifier. type: string id: + description: The saved object identifier. type: string ignoreMissingReferences: + description: >- + When set to true, any missing references errors are + ignored. type: boolean overwrite: default: false + description: >- + When set to true, the saved object from the source + space overwrites the conflicting object in the + destination space. type: boolean type: + description: The saved object type. type: string required: - type @@ -21071,11 +21127,11 @@ paths: - retries - objects responses: {} - summary: '' + summary: Resolve conflicts copying saved objects tags: [] /api/spaces/_update_objects_spaces: post: - description: Update saved objects in spaces + description: Update one or more saved objects to add or remove them from some spaces. operationId: '%2Fapi%2Fspaces%2F_update_objects_spaces#0' parameters: - description: The version of the API to use @@ -21106,8 +21162,10 @@ paths: type: object properties: id: + description: The identifier of the saved object to update. type: string type: + description: The type of the saved object to update. type: string required: - type @@ -21115,10 +21173,16 @@ paths: type: array spacesToAdd: items: + description: >- + The identifiers of the spaces the saved objects should be + added to or removed from. type: string type: array spacesToRemove: items: + description: >- + The identifiers of the spaces the saved objects should be + added to or removed from. type: string type: array required: @@ -21126,11 +21190,11 @@ paths: - spacesToAdd - spacesToRemove responses: {} - summary: '' - tags: [] + summary: Update saved objects in spaces + tags: + - spaces /api/spaces/space: get: - description: Get all spaces operationId: '%2Fapi%2Fspaces%2Fspace#0' parameters: - description: The version of the API to use @@ -21141,7 +21205,10 @@ paths: enum: - '2023-10-31' type: string - - in: query + - description: >- + Specifies which authorization checks are applied to the API call. + The default value is `any`. + in: query name: purpose required: false schema: @@ -21150,7 +21217,15 @@ paths: - copySavedObjectsIntoSpace - shareSavedObjectsIntoSpace type: string - - in: query + - description: >- + When enabled, the API returns any spaces that the user is authorized + to access in any capacity and each space will contain the purposes + for which the user is authorized. This can be useful to determine + which spaces a user can read but not take a specific action in. If + the security plugin is not enabled, this parameter has no effect, + since no authorization checks take place. This parameter cannot be + used in with the `purpose` parameter. + in: query name: include_authorized_purposes required: true schema: @@ -21169,12 +21244,13 @@ paths: x-oas-optional: true - type: boolean x-oas-optional: true - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get all spaces tags: - spaces post: - description: Create a space operationId: '%2Fapi%2Fspaces%2Fspace#1' parameters: - description: The version of the API to use @@ -21202,22 +21278,45 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string solution: @@ -21230,13 +21329,17 @@ paths: required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Create a space tags: - spaces '/api/spaces/space/{id}': delete: - description: Delete a space + description: >- + When you delete a space, all saved objects that belong to the space are + automatically deleted, which is permanent and cannot be undone. operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2' parameters: - description: The version of the API to use @@ -21254,17 +21357,21 @@ paths: schema: example: 'true' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '204': + description: Indicates a successful call. + '404': + description: Indicates that the request failed. + summary: Delete a space tags: - spaces get: - description: Get a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0' parameters: - description: The version of the API to use @@ -21275,17 +21382,19 @@ paths: enum: - '2023-10-31' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get a space tags: - spaces put: - description: Update a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1' parameters: - description: The version of the API to use @@ -21303,7 +21412,10 @@ paths: schema: example: 'true' type: string - - in: path + - description: >- + The space identifier. You are unable to change the ID with the + update operation. + in: path name: id required: true schema: @@ -21318,22 +21430,45 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string solution: @@ -21346,8 +21481,10 @@ paths: required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Update a space tags: - spaces /api/status: @@ -40452,6 +40589,7 @@ components: Security_Timeline_API_AssociatedFilterType: description: Filter notes based on their association with a document or saved object. enum: + - all - document_only - saved_object_only - document_and_saved_object diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 647ad65e617e0..f7c5e34257336 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -20841,7 +20841,13 @@ paths: - roles /api/spaces/_copy_saved_objects: post: - description: Copy saved objects to spaces + description: >- + It also allows you to automatically copy related objects, so when you + copy a dashboard, this can automatically copy over the associated + visualizations, data views, and saved searches, as required. You can + request to overwrite any objects that already exist in the target space + if they share an identifier or you can use the resolve copy saved + objects conflicts API to do this on a per-object basis. operationId: '%2Fapi%2Fspaces%2F_copy_saved_objects#0' parameters: - description: The version of the API to use @@ -20868,12 +20874,26 @@ paths: properties: compatibilityMode: default: false + description: >- + Apply various adjustments to the saved objects that are + being copied to maintain compatibility between different + Kibana versions. Use this option only if you encounter + issues with copied saved objects. This option cannot be used + with the `createNewCopies` option. type: boolean createNewCopies: default: true + description: >- + Create new copies of saved objects, regenerate each object + identifier, and reset the origin. When used, potential + conflict errors are avoided. This option cannot be used + with the `overwrite` and `compatibilityMode` options. type: boolean includeReferences: default: false + description: >- + When set to true, all saved objects related to the specified + saved objects will also be copied into the target spaces. type: boolean objects: items: @@ -20881,8 +20901,10 @@ paths: type: object properties: id: + description: The identifier of the saved object to copy. type: string type: + description: The type of the saved object to copy. type: string required: - type @@ -20890,20 +20912,29 @@ paths: type: array overwrite: default: false + description: >- + When set to true, all conflicts are automatically + overridden. When a saved object with a matching type and + identifier exists in the target space, that version is + replaced with the version from the source space. This option + cannot be used with the `createNewCopies` option. type: boolean spaces: items: + description: >- + The identifiers of the spaces where you want to copy the + specified objects. type: string type: array required: - spaces - objects responses: {} - summary: '' - tags: [] + summary: Copy saved objects between spaces + tags: + - spaces /api/spaces/_disable_legacy_url_aliases: post: - description: Disable legacy URL aliases operationId: '%2Fapi%2Fspaces%2F_disable_legacy_url_aliases#0' parameters: - description: The version of the API to use @@ -20934,10 +20965,15 @@ paths: type: object properties: sourceId: + description: >- + The alias source object identifier. This is the legacy + object identifier. type: string targetSpace: + description: The space where the alias target object exists. type: string targetType: + description: 'The type of alias target object. ' type: string required: - targetSpace @@ -20947,11 +20983,12 @@ paths: required: - aliases responses: {} - summary: '' - tags: [] + summary: Disable legacy URL aliases + tags: + - spaces /api/spaces/_get_shareable_references: post: - description: Get shareable references + description: Collect references and space contexts for saved objects. operationId: '%2Fapi%2Fspaces%2F_get_shareable_references#0' parameters: - description: The version of the API to use @@ -20992,11 +21029,14 @@ paths: required: - objects responses: {} - summary: '' - tags: [] + summary: Get shareable references + tags: + - spaces /api/spaces/_resolve_copy_saved_objects_errors: post: - description: Resolve conflicts copying saved objects + description: >- + Overwrite saved objects that are returned as errors from the copy saved + objects to space API. operationId: '%2Fapi%2Fspaces%2F_resolve_copy_saved_objects_errors#0' parameters: - description: The version of the API to use @@ -21050,17 +21090,33 @@ paths: type: object properties: createNewCopy: + description: >- + Creates new copies of the saved objects, regenerates + each object ID, and resets the origin. type: boolean destinationId: + description: >- + Specifies the destination identifier that the copied + object should have, if different from the current + identifier. type: string id: + description: The saved object identifier. type: string ignoreMissingReferences: + description: >- + When set to true, any missing references errors are + ignored. type: boolean overwrite: default: false + description: >- + When set to true, the saved object from the source + space overwrites the conflicting object in the + destination space. type: boolean type: + description: The saved object type. type: string required: - type @@ -21071,11 +21127,11 @@ paths: - retries - objects responses: {} - summary: '' + summary: Resolve conflicts copying saved objects tags: [] /api/spaces/_update_objects_spaces: post: - description: Update saved objects in spaces + description: Update one or more saved objects to add or remove them from some spaces. operationId: '%2Fapi%2Fspaces%2F_update_objects_spaces#0' parameters: - description: The version of the API to use @@ -21106,8 +21162,10 @@ paths: type: object properties: id: + description: The identifier of the saved object to update. type: string type: + description: The type of the saved object to update. type: string required: - type @@ -21115,10 +21173,16 @@ paths: type: array spacesToAdd: items: + description: >- + The identifiers of the spaces the saved objects should be + added to or removed from. type: string type: array spacesToRemove: items: + description: >- + The identifiers of the spaces the saved objects should be + added to or removed from. type: string type: array required: @@ -21126,11 +21190,11 @@ paths: - spacesToAdd - spacesToRemove responses: {} - summary: '' - tags: [] + summary: Update saved objects in spaces + tags: + - spaces /api/spaces/space: get: - description: Get all spaces operationId: '%2Fapi%2Fspaces%2Fspace#0' parameters: - description: The version of the API to use @@ -21141,7 +21205,10 @@ paths: enum: - '2023-10-31' type: string - - in: query + - description: >- + Specifies which authorization checks are applied to the API call. + The default value is `any`. + in: query name: purpose required: false schema: @@ -21150,7 +21217,15 @@ paths: - copySavedObjectsIntoSpace - shareSavedObjectsIntoSpace type: string - - in: query + - description: >- + When enabled, the API returns any spaces that the user is authorized + to access in any capacity and each space will contain the purposes + for which the user is authorized. This can be useful to determine + which spaces a user can read but not take a specific action in. If + the security plugin is not enabled, this parameter has no effect, + since no authorization checks take place. This parameter cannot be + used in with the `purpose` parameter. + in: query name: include_authorized_purposes required: true schema: @@ -21169,12 +21244,13 @@ paths: x-oas-optional: true - type: boolean x-oas-optional: true - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get all spaces tags: - spaces post: - description: Create a space operationId: '%2Fapi%2Fspaces%2Fspace#1' parameters: - description: The version of the API to use @@ -21202,22 +21278,45 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string solution: @@ -21230,13 +21329,17 @@ paths: required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Create a space tags: - spaces '/api/spaces/space/{id}': delete: - description: Delete a space + description: >- + When you delete a space, all saved objects that belong to the space are + automatically deleted, which is permanent and cannot be undone. operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#2' parameters: - description: The version of the API to use @@ -21254,17 +21357,21 @@ paths: schema: example: 'true' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '204': + description: Indicates a successful call. + '404': + description: Indicates that the request failed. + summary: Delete a space tags: - spaces get: - description: Get a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#0' parameters: - description: The version of the API to use @@ -21275,17 +21382,19 @@ paths: enum: - '2023-10-31' type: string - - in: path + - description: The space identifier. + in: path name: id required: true schema: type: string - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Get a space tags: - spaces put: - description: Update a space operationId: '%2Fapi%2Fspaces%2Fspace%2F%7Bid%7D#1' parameters: - description: The version of the API to use @@ -21303,7 +21412,10 @@ paths: schema: example: 'true' type: string - - in: path + - description: >- + The space identifier. You are unable to change the ID with the + update operation. + in: path name: id required: true schema: @@ -21318,22 +21430,45 @@ paths: _reserved: type: boolean color: + description: >- + The hexadecimal color code used in the space avatar. By + default, the color is automatically generated from the space + name. type: string description: + description: A description for the space. type: string disabledFeatures: default: [] items: + description: The list of features that are turned off in the space. type: string type: array id: + description: >- + The space ID that is part of the Kibana URL when inside the + space. Space IDs are limited to lowercase alphanumeric, + underscore, and hyphen characters (a-z, 0-9, _, and -). You + are cannot change the ID with the update operation. type: string imageUrl: + description: >- + The data-URL encoded image to display in the space avatar. + If specified, initials will not be displayed and the color + will be visible as the background color for transparent + images. For best results, your image should be 64x64. Images + will not be optimized by this API call, so care should be + taken when using custom images. type: string initials: + description: >- + One or two characters that are shown in the space avatar. By + default, the initials are automatically generated from the + space name. maxLength: 2 type: string name: + description: 'The display name for the space. ' minLength: 1 type: string solution: @@ -21346,8 +21481,10 @@ paths: required: - id - name - responses: {} - summary: '' + responses: + '200': + description: Indicates a successful call. + summary: Update a space tags: - spaces /api/status: @@ -40452,6 +40589,7 @@ components: Security_Timeline_API_AssociatedFilterType: description: Filter notes based on their association with a document or saved object. enum: + - all - document_only - saved_object_only - document_and_saved_object diff --git a/oas_docs/overlays/kibana.overlays.shared.yaml b/oas_docs/overlays/kibana.overlays.shared.yaml new file mode 100644 index 0000000000000..5f16b6dfa6719 --- /dev/null +++ b/oas_docs/overlays/kibana.overlays.shared.yaml @@ -0,0 +1,47 @@ +# overlays.yaml +overlay: 1.0.0 +info: + title: Overlays that are applicable to both serverless and non-serverless documentas + version: 0.0.1 +actions: +# Add some spaces API examples + - target: "$.paths['/api/spaces/space']['post']" + description: "Add example to create space API" + update: + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + createSpaceRequest: + $ref: "../examples/create_space_request.yaml" + - target: "$.paths['/api/spaces/space/{id}']['put']" + description: "Add example to update space API" + update: + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + updateSpaceRequest: + $ref: "../examples/update_space_request.yaml" + - target: "$.paths['/api/spaces/space/{id}']['get']" + description: "Add example to get space API" + update: + responses: + 200: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + getSpaceResponseExample: + $ref: "../examples/get_space_response.yaml" + - target: "$.paths['/api/spaces/space']['get']" + description: "Add example to get all spaces API" + update: + responses: + 200: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + getSpacesResponseExample1: + $ref: "../examples/get_spaces_response1.yaml" + getSpacesResponseExample2: + $ref: "../examples/get_spaces_response2.yaml" diff --git a/oas_docs/overlays/kibana.overlays.yaml b/oas_docs/overlays/kibana.overlays.yaml index c4747d7d13a0a..4a21c029ef80a 100644 --- a/oas_docs/overlays/kibana.overlays.yaml +++ b/oas_docs/overlays/kibana.overlays.yaml @@ -4,7 +4,7 @@ info: title: Overlays for the Kibana API document version: 0.0.1 actions: - # Add an introduction to spaces +# Add an introduction to spaces - target: '$' description: Add an extra page about spaces update: @@ -24,7 +24,7 @@ actions: If you use the Kibana console to send API requests, it automatically adds the appropriate space identifier. To learn more, check out [Spaces](https://www.elastic.co/guide/en/kibana/master/xpack-spaces.html). - # Add some tag descriptions and displayNames +# Add some tag descriptions and displayNames - target: '$.tags[?(@.name=="alerting")]' description: Change tag description and displayName update: @@ -91,7 +91,77 @@ actions: x-displayName: "System" description: > Get information about the system status, resource usage, and installed plugins. - # Remove extra tags from operations +# Remove extra tags from operations - target: "$.paths[*][*].tags[1:]" description: Remove all but first tag from operations - remove: true \ No newline at end of file + remove: true +# Examples that are not applicable to serverless + - target: "$.paths['/api/spaces/_copy_saved_objects']['post']" + description: "Add example to copy saved objects to space API" + update: + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + copySavedObjectsRequestExample1: + $ref: "../examples/copy_saved_objects_request1.yaml" + copySavedObjectsRequestExample2: + $ref: "../examples/copy_saved_objects_request2.yaml" + responses: + 200: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + copySavedObjectsResponseExample1: + $ref: "../examples/copy_saved_objects_response1.yaml" + copySavedObjectsResponseExample2: + $ref: "../examples/copy_saved_objects_response2.yaml" + copySavedObjectsResponseExample3: + $ref: "../examples/copy_saved_objects_response3.yaml" + copySavedObjectsResponseExample4: + $ref: "../examples/copy_saved_objects_response4.yaml" + - target: "$.paths['/api/spaces/_resolve_copy_saved_objects_errors']['post']" + description: "Add example to resolve copy saved objects to space conflicts API" + update: + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + resolveCopySavedObjectsRequestExample1: + $ref: "../examples/resolve_copy_saved_objects_request1.yaml" + resolveCopySavedObjectsRequestExample2: + $ref: "../examples/resolve_copy_saved_objects_request2.yaml" + responses: + 200: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + resolveCopySavedObjectsResponseExample1: + $ref: "../examples/copy_saved_objects_response1.yaml" + resolveCopySavedObjectsResponseExample2: + $ref: "../examples/copy_saved_objects_response2.yaml" + - target: "$.paths['/api/spaces/_disable_legacy_url_aliases']['post']" + description: "Add example to disable legacy URL aliases API" + update: + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + disableLegacyURLRequestExample1: + $ref: "../examples/disable_legacy_url_request1.yaml" + - target: "$.paths['/api/spaces/_update_objects_spaces']['post']" + description: "Add example to update saved object spaces API" + update: + requestBody: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + updateObjectSpacesRequestExample1: + $ref: "../examples/update_saved_objects_spaces_request1.yaml" + responses: + 200: + content: + application/json; Elastic-Api-Version=2023-10-31: + examples: + updateObjectSpacesResponseExample1: + $ref: "../examples/update_saved_objects_spaces_response1.yaml" \ No newline at end of file diff --git a/package.json b/package.json index 1f929c6d59f9e..4dc96d20db296 100644 --- a/package.json +++ b/package.json @@ -1766,7 +1766,7 @@ "json5": "^2.2.3", "jsondiffpatch": "0.4.1", "license-checker": "^25.0.1", - "listr2": "^8.2.4", + "listr2": "^8.2.5", "lmdb": "^2.9.2", "loader-utils": "^2.0.4", "marge": "^1.0.1", diff --git a/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts b/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts index 9673d1678132b..9e10bd5388637 100644 --- a/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/logs/logs_synthtrace_es_client.ts @@ -48,6 +48,33 @@ export class LogsSynthtraceEsClient extends SynthtraceEsClient { } } + async createComponentTemplate(name: string, mappings: MappingTypeMapping) { + const isTemplateExisting = await this.client.cluster.existsComponentTemplate({ name }); + + if (isTemplateExisting) return this.logger.info(`Component template already exists: ${name}`); + + try { + await this.client.cluster.putComponentTemplate({ + name, + template: { + mappings, + }, + }); + this.logger.info(`Component template successfully created: ${name}`); + } catch (err) { + this.logger.error(`Component template creation failed: ${name} - ${err.message}`); + } + } + + async deleteComponentTemplate(name: string) { + try { + await this.client.cluster.deleteComponentTemplate({ name }); + this.logger.info(`Component template successfully deleted: ${name}`); + } catch (err) { + this.logger.error(`Component template deletion failed: ${name} - ${err.message}`); + } + } + async createIndex(index: string, mappings?: MappingTypeMapping) { try { const isIndexExisting = await this.client.indices.exists({ index }); diff --git a/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts index 101661973a692..15e0496a9087c 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts @@ -187,5 +187,21 @@ describe('FROM', () => { expect(errors.length > 0).toBe(true); }); + + it('when open square bracket "[" is entered', () => { + const text = 'FROM kibana_sample_data_logs ['; + const { errors } = parse(text); + + expect(errors.length > 0).toBe(true); + expect(errors[0].message.toLowerCase().includes('metadata')).toBe(true); + }); + + it('when close square bracket "]" is entered', () => { + const text = 'FROM kibana_sample_data_logs []'; + const { errors } = parse(text); + + expect(errors.length > 0).toBe(true); + expect(errors[0].message.toLowerCase().includes('metadata')).toBe(true); + }); }); }); diff --git a/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts index 514d769d5c45e..ddce3785eb1bf 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts @@ -24,7 +24,7 @@ describe('literal expression', () => { }); }); - it('decimals vs integers', () => { + it('doubles vs integers', () => { const text = 'ROW a(1.0, 1)'; const { ast } = parse(text); @@ -36,7 +36,7 @@ describe('literal expression', () => { args: [ { type: 'literal', - literalType: 'decimal', + literalType: 'double', }, { type: 'literal', diff --git a/packages/kbn-esql-ast/src/parser/esql_ast_builder_listener.ts b/packages/kbn-esql-ast/src/parser/esql_ast_builder_listener.ts index de406e33aa7a5..3959d42d8a35f 100644 --- a/packages/kbn-esql-ast/src/parser/esql_ast_builder_listener.ts +++ b/packages/kbn-esql-ast/src/parser/esql_ast_builder_listener.ts @@ -120,7 +120,7 @@ export class ESQLAstBuilderListener implements ESQLParserListener { const metadataContext = ctx.metadata(); const metadataContent = metadataContext?.deprecated_metadata()?.metadataOption() || metadataContext?.metadataOption(); - if (metadataContent) { + if (metadataContent && metadataContent.METADATA()) { const option = createOption( metadataContent.METADATA().getText().toLowerCase(), metadataContent diff --git a/packages/kbn-esql-ast/src/parser/factories.ts b/packages/kbn-esql-ast/src/parser/factories.ts index 321ca6a40dcd0..0fffb3a970e4c 100644 --- a/packages/kbn-esql-ast/src/parser/factories.ts +++ b/packages/kbn-esql-ast/src/parser/factories.ts @@ -41,6 +41,7 @@ import type { FunctionSubtype, ESQLNumericLiteral, ESQLOrderExpression, + InlineCastingType, } from '../types'; import { parseIdentifier, getPosition } from './helpers'; import { Builder, type AstNodeParserFields } from '../builder'; @@ -72,7 +73,7 @@ export const createCommand = (name: string, ctx: ParserRuleContext) => export const createInlineCast = (ctx: InlineCastContext, value: ESQLInlineCast['value']) => Builder.expression.inlineCast( - { castType: ctx.dataType().getText(), value }, + { castType: ctx.dataType().getText().toLowerCase() as InlineCastingType, value }, createParserFields(ctx) ); @@ -107,7 +108,7 @@ export function createLiteralString(token: Token): ESQLLiteral { const text = token.text!; return { type: 'literal', - literalType: 'string', + literalType: 'keyword', text, name: text, value: text, @@ -149,13 +150,13 @@ export function createLiteral( location: getPosition(node.symbol), incomplete: isMissingText(text), }; - if (type === 'decimal' || type === 'integer') { + if (type === 'double' || type === 'integer') { return { ...partialLiteral, literalType: type, value: Number(text), paramType: 'number', - } as ESQLNumericLiteral<'decimal'> | ESQLNumericLiteral<'integer'>; + } as ESQLNumericLiteral<'double'> | ESQLNumericLiteral<'integer'>; } else if (type === 'param') { throw new Error('Should never happen'); } diff --git a/packages/kbn-esql-ast/src/parser/walkers.ts b/packages/kbn-esql-ast/src/parser/walkers.ts index cccc215ec365e..30c17c56483f8 100644 --- a/packages/kbn-esql-ast/src/parser/walkers.ts +++ b/packages/kbn-esql-ast/src/parser/walkers.ts @@ -346,7 +346,7 @@ function getConstant(ctx: ConstantContext): ESQLAstItem { // Decimal type covers multiple ES|QL types: long, double, etc. if (ctx instanceof DecimalLiteralContext) { - return createNumericLiteral(ctx.decimalValue(), 'decimal'); + return createNumericLiteral(ctx.decimalValue(), 'double'); } // Integer type encompasses integer @@ -358,7 +358,7 @@ function getConstant(ctx: ConstantContext): ESQLAstItem { } if (ctx instanceof StringLiteralContext) { // String literal covers multiple ES|QL types: text and keyword types - return createLiteral('string', ctx.string_().QUOTED_STRING()); + return createLiteral('keyword', ctx.string_().QUOTED_STRING()); } if ( ctx instanceof NumericArrayLiteralContext || @@ -371,14 +371,14 @@ function getConstant(ctx: ConstantContext): ESQLAstItem { const isDecimal = numericValue.decimalValue() !== null && numericValue.decimalValue() !== undefined; const value = numericValue.decimalValue() || numericValue.integerValue(); - values.push(createNumericLiteral(value!, isDecimal ? 'decimal' : 'integer')); + values.push(createNumericLiteral(value!, isDecimal ? 'double' : 'integer')); } for (const booleanValue of ctx.getTypedRuleContexts(BooleanValueContext)) { values.push(getBooleanValue(booleanValue)!); } for (const string of ctx.getTypedRuleContexts(StringContext)) { // String literal covers multiple ES|QL types: text and keyword types - const literal = createLiteral('string', string.QUOTED_STRING()); + const literal = createLiteral('keyword', string.QUOTED_STRING()); if (literal) { values.push(literal); } @@ -534,7 +534,7 @@ function collectRegexExpression(ctx: BooleanExpressionContext): ESQLFunction[] { const arg = visitValueExpression(regex.valueExpression()); if (arg) { fn.args.push(arg); - const literal = createLiteral('string', regex._pattern.QUOTED_STRING()); + const literal = createLiteral('keyword', regex._pattern.QUOTED_STRING()); if (literal) { fn.args.push(literal); } @@ -672,7 +672,7 @@ export function visitDissect(ctx: DissectCommandContext) { return [ visitPrimaryExpression(ctx.primaryExpression()), ...(pattern && textExistsAndIsValid(pattern.getText()) - ? [createLiteral('string', pattern), ...visitDissectOptions(ctx.commandOptions())] + ? [createLiteral('keyword', pattern), ...visitDissectOptions(ctx.commandOptions())] : []), ].filter(nonNullable); } @@ -682,7 +682,7 @@ export function visitGrok(ctx: GrokCommandContext) { return [ visitPrimaryExpression(ctx.primaryExpression()), ...(pattern && textExistsAndIsValid(pattern.getText()) - ? [createLiteral('string', pattern)] + ? [createLiteral('keyword', pattern)] : []), ].filter(nonNullable); } diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.comments.test.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.comments.test.ts index 3ac79acda8af3..861d274493a42 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.comments.test.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.comments.test.ts @@ -399,7 +399,7 @@ ROW // 2 /* 3 */ // 4 - /* 5 */ /* 6 */ 1::INTEGER /* 7 */ /* 8 */ // 9`); + /* 5 */ /* 6 */ 1::integer /* 7 */ /* 8 */ // 9`); }); }); diff --git a/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts b/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts index eb7eaf1075c70..3c12de90e4454 100644 --- a/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/leaf_printer.ts @@ -64,10 +64,10 @@ export const LeafPrinter = { return '?'; } } - case 'string': { + case 'keyword': { return String(node.value); } - case 'decimal': { + case 'double': { const isRounded = node.value % 1 === 0; if (isRounded) { diff --git a/packages/kbn-esql-ast/src/types.ts b/packages/kbn-esql-ast/src/types.ts index 1bac6e0cff5b3..0df75ee2e8f24 100644 --- a/packages/kbn-esql-ast/src/types.ts +++ b/packages/kbn-esql-ast/src/types.ts @@ -193,10 +193,33 @@ export type BinaryExpressionAssignmentOperator = '='; export type BinaryExpressionComparisonOperator = '==' | '=~' | '!=' | '<' | '<=' | '>' | '>='; export type BinaryExpressionRegexOperator = 'like' | 'not_like' | 'rlike' | 'not_rlike'; +// from https://github.com/elastic/elasticsearch/blob/122e7288200ee03e9087c98dff6cebbc94e774aa/docs/reference/esql/functions/kibana/inline_cast.json +export type InlineCastingType = + | 'bool' + | 'boolean' + | 'cartesian_point' + | 'cartesian_shape' + | 'date_nanos' + | 'date_period' + | 'datetime' + | 'double' + | 'geo_point' + | 'geo_shape' + | 'int' + | 'integer' + | 'ip' + | 'keyword' + | 'long' + | 'string' + | 'text' + | 'time_duration' + | 'unsigned_long' + | 'version'; + export interface ESQLInlineCast extends ESQLAstBaseItem { type: 'inlineCast'; value: ValueType; - castType: string; + castType: InlineCastingType; } /** @@ -270,7 +293,7 @@ export interface ESQLList extends ESQLAstBaseItem { values: ESQLLiteral[]; } -export type ESQLNumericLiteralType = 'decimal' | 'integer'; +export type ESQLNumericLiteralType = 'double' | 'integer'; export type ESQLLiteral = | ESQLDecimalLiteral @@ -290,7 +313,7 @@ export interface ESQLNumericLiteral extends ES } // We cast anything as decimal (e.g. 32.12) as generic decimal numeric type here // @internal -export type ESQLDecimalLiteral = ESQLNumericLiteral<'decimal'>; +export type ESQLDecimalLiteral = ESQLNumericLiteral<'double'>; // @internal export type ESQLIntegerLiteral = ESQLNumericLiteral<'integer'>; @@ -312,7 +335,7 @@ export interface ESQLNullLiteral extends ESQLAstBaseItem { // @internal export interface ESQLStringLiteral extends ESQLAstBaseItem { type: 'literal'; - literalType: 'string'; + literalType: 'keyword'; value: string; } diff --git a/packages/kbn-esql-ast/src/visitor/contexts.ts b/packages/kbn-esql-ast/src/visitor/contexts.ts index 4b4f04fdca4bb..086a217d8f117 100644 --- a/packages/kbn-esql-ast/src/visitor/contexts.ts +++ b/packages/kbn-esql-ast/src/visitor/contexts.ts @@ -337,7 +337,7 @@ export class LimitCommandVisitorContext< if ( arg && arg.type === 'literal' && - (arg.literalType === 'integer' || arg.literalType === 'decimal') + (arg.literalType === 'integer' || arg.literalType === 'double') ) { return arg; } diff --git a/packages/kbn-esql-ast/src/walker/walker.test.ts b/packages/kbn-esql-ast/src/walker/walker.test.ts index 9900f586dc4a0..8dd40b1a87bd1 100644 --- a/packages/kbn-esql-ast/src/walker/walker.test.ts +++ b/packages/kbn-esql-ast/src/walker/walker.test.ts @@ -342,7 +342,7 @@ describe('structurally can walk all nodes', () => { }, { type: 'literal', - literalType: 'string', + literalType: 'keyword', name: '"foo"', }, { @@ -375,7 +375,7 @@ describe('structurally can walk all nodes', () => { }, { type: 'literal', - literalType: 'string', + literalType: 'keyword', name: '"2"', }, { @@ -390,7 +390,7 @@ describe('structurally can walk all nodes', () => { }, { type: 'literal', - literalType: 'decimal', + literalType: 'double', name: '3.14', }, ]); @@ -473,7 +473,7 @@ describe('structurally can walk all nodes', () => { values: [ { type: 'literal', - literalType: 'decimal', + literalType: 'double', name: '3.3', }, ], @@ -492,7 +492,7 @@ describe('structurally can walk all nodes', () => { }, { type: 'literal', - literalType: 'decimal', + literalType: 'double', name: '3.3', }, ]); @@ -600,27 +600,27 @@ describe('structurally can walk all nodes', () => { expect(literals).toMatchObject([ { type: 'literal', - literalType: 'string', + literalType: 'keyword', name: '"a"', }, { type: 'literal', - literalType: 'string', + literalType: 'keyword', name: '"b"', }, { type: 'literal', - literalType: 'string', + literalType: 'keyword', name: '"c"', }, { type: 'literal', - literalType: 'string', + literalType: 'keyword', name: '"d"', }, { type: 'literal', - literalType: 'string', + literalType: 'keyword', name: '"e"', }, ]); diff --git a/packages/kbn-esql-validation-autocomplete/scripts/generate_function_definitions.ts b/packages/kbn-esql-validation-autocomplete/scripts/generate_function_definitions.ts index 13f0b2c66ce32..fe2a85456aa12 100644 --- a/packages/kbn-esql-validation-autocomplete/scripts/generate_function_definitions.ts +++ b/packages/kbn-esql-validation-autocomplete/scripts/generate_function_definitions.ts @@ -52,7 +52,7 @@ const extraFunctions: FunctionDefinition[] = [ { name: 'value', type: 'any' }, ], minParams: 2, - returnType: 'any', + returnType: 'unknown', }, ], examples: [ diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index 6b2f5fea0cc8d..b3884f5cb96be 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -259,7 +259,6 @@ describe('autocomplete.suggest', () => { ...getFieldNamesByType('integer'), ...getFieldNamesByType('double'), ...getFieldNamesByType('long'), - '`avg(b)`', ...getFunctionSignaturesByReturnType('eval', ['integer', 'double', 'long'], { scalar: true, }), @@ -284,11 +283,19 @@ describe('autocomplete.suggest', () => { const { assertSuggestions } = await setup(); await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '| ']); + await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '| '], { + triggerCharacter: ' ', + }); await assertSuggestions( 'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day)/', [',', '| ', '+ $0', '- $0'] ); + await assertSuggestions( + 'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day) /', + [',', '| ', '+ $0', '- $0'], + { triggerCharacter: ' ' } + ); }); test('on space within bucket()', async () => { const { assertSuggestions } = await setup(); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 5d885379f1a94..4b732d25151da 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -1770,10 +1770,15 @@ async function getOptionArgsSuggestions( innerText, command, option, - { type: argDef?.type || 'any' }, + { type: argDef?.type || 'unknown' }, nodeArg, nodeArgType as string, - references, + { + fields: references.fields, + // you can't use a variable defined + // in the stats command in the by clause + variables: new Map(), + }, getFieldsByType )) ); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts index 42bb02058023b..662b84c4a788f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts @@ -64,7 +64,6 @@ export const getBuiltinCompatibleFunctionDefinition = ( const compatibleFunctions = [...builtinFunctions, ...getTestFunctions()].filter( ({ name, supportedCommands, supportedOptions, signatures, ignoreAsSuggestion }) => !ignoreAsSuggestion && - !/not_/.test(name) && (!skipAssign || name !== '=') && (option ? supportedOptions?.includes(option) : supportedCommands.includes(command)) && signatures.some( @@ -78,7 +77,10 @@ export const getBuiltinCompatibleFunctionDefinition = ( return compatibleFunctions .filter((mathDefinition) => mathDefinition.signatures.some( - (signature) => returnTypes[0] === 'any' || returnTypes.includes(signature.returnType) + (signature) => + returnTypes[0] === 'unknown' || + returnTypes[0] === 'any' || + returnTypes.includes(signature.returnType) ) ) .map(getSuggestionBuiltinDefinition); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts index 41f6a92dc313d..dd450e28b66a9 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts @@ -52,23 +52,6 @@ export function getFunctionsToIgnoreForStats(command: ESQLCommand, argIndex: num return isFunctionItem(arg) ? getFnContent(arg) : []; } -/** - * Given a function signature, returns the parameter at the given position. - * - * Takes into account variadic functions (minParams), returning the last - * parameter if the position is greater than the number of parameters. - * - * @param signature - * @param position - * @returns - */ -export function getParamAtPosition( - { params, minParams }: FunctionDefinition['signatures'][number], - position: number -) { - return params.length > position ? params[position] : minParams ? params[params.length - 1] : null; -} - /** * Given a function signature, returns the parameter at the given position, even if it's undefined or null * diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts index c59daa2130417..e71ed32e4c79d 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/builtin.ts @@ -423,6 +423,20 @@ const likeFunctions: FunctionDefinition[] = [ ], returnType: 'boolean', }, + { + params: [ + { name: 'left', type: 'text' as const }, + { name: 'right', type: 'keyword' as const }, + ], + returnType: 'boolean', + }, + { + params: [ + { name: 'left', type: 'keyword' as const }, + { name: 'right', type: 'text' as const }, + ], + returnType: 'boolean', + }, { params: [ { name: 'left', type: 'keyword' as const }, @@ -609,25 +623,12 @@ const otherDefinitions: FunctionDefinition[] = [ { name: 'left', type: 'any' }, { name: 'right', type: 'any' }, ], - returnType: 'void', - }, - ], - }, - { - name: 'functions', - type: 'builtin', - description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.functionsDoc', { - defaultMessage: 'Show ES|QL avaialble functions with signatures', - }), - supportedCommands: ['meta'], - signatures: [ - { - params: [], - returnType: 'void', + returnType: 'unknown', }, ], }, { + // TODO — this shouldn't be a function or an operator... name: 'info', type: 'builtin', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.infoDoc', { @@ -637,21 +638,7 @@ const otherDefinitions: FunctionDefinition[] = [ signatures: [ { params: [], - returnType: 'void', - }, - ], - }, - { - name: 'order-expression', - type: 'builtin', - description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.infoDoc', { - defaultMessage: 'Specify column sorting modifiers', - }), - supportedCommands: ['sort'], - signatures: [ - { - params: [{ name: 'column', type: 'any' }], - returnType: 'void', + returnType: 'unknown', // meaningless }, ], }, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/generated/scalar_functions.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/generated/scalar_functions.ts index b25d3ad8b6563..ea5f8f86e1909 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/generated/scalar_functions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/generated/scalar_functions.ts @@ -9033,7 +9033,7 @@ const caseDefinition: FunctionDefinition = { }, ], minParams: 2, - returnType: 'any', + returnType: 'unknown', }, ], supportedCommands: ['stats', 'inlinestats', 'metrics', 'eval', 'where', 'row', 'sort'], diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/options.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/options.ts index 2e6fbc791b747..31d443a8cbb2b 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/options.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/options.ts @@ -129,7 +129,7 @@ export const appendSeparatorOption: CommandOptionsDefinition = { const [firstArg] = option.args; if ( !Array.isArray(firstArg) && - (!isLiteralItem(firstArg) || firstArg.literalType !== 'string') + (!isLiteralItem(firstArg) || firstArg.literalType !== 'keyword') ) { const value = 'value' in firstArg && !isInlineCastItem(firstArg) ? firstArg.value : firstArg.name; diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index 9ce286796c971..dee08766745df 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -100,12 +100,14 @@ export const isParameterType = (str: string | undefined): str is FunctionParamet /** * This is the return type of a function definition. + * + * TODO: remove `any` */ -export type FunctionReturnType = Exclude | 'any' | 'void'; +export type FunctionReturnType = Exclude | 'unknown' | 'any'; export const isReturnType = (str: string | FunctionParameterType): str is FunctionReturnType => str !== 'unsupported' && - (dataTypes.includes(str as SupportedDataType) || str === 'any' || str === 'void'); + (dataTypes.includes(str as SupportedDataType) || str === 'unknown' || str === 'any'); export interface FunctionDefinition { type: 'builtin' | 'agg' | 'eval'; diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts index 0f7f830c1417a..1c2e9075e95ff 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -153,7 +153,7 @@ function isBuiltinFunction(node: ESQLFunction) { export function getAstContext(queryString: string, ast: ESQLAst, offset: number) { const { command, option, setting, node } = findAstPosition(ast, offset); if (node) { - if (node.type === 'literal' && node.literalType === 'string') { + if (node.type === 'literal' && node.literalType === 'keyword') { // command ... "" return { type: 'value' as const, command, node, option, setting }; } diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/esql_types.ts b/packages/kbn-esql-validation-autocomplete/src/shared/esql_types.ts index 66f985505a43d..dbf45437dce92 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/esql_types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/esql_types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ESQLDecimalLiteral, ESQLLiteral, ESQLNumericLiteralType } from '@kbn/esql-ast/src/types'; +import { ESQLLiteral, ESQLNumericLiteralType } from '@kbn/esql-ast/src/types'; import { FunctionParameterType } from '../definitions/types'; export const ESQL_COMMON_NUMERIC_TYPES = ['double', 'long', 'integer'] as const; @@ -27,15 +27,6 @@ export const ESQL_NUMBER_TYPES = [ export const ESQL_STRING_TYPES = ['keyword', 'text'] as const; export const ESQL_DATE_TYPES = ['datetime', 'date_period'] as const; -/** - * - * @param type - * @returns - */ -export function isStringType(type: unknown) { - return typeof type === 'string' && ['keyword', 'text'].includes(type); -} - export function isNumericType(type: unknown): type is ESQLNumericLiteralType { return ( typeof type === 'string' && @@ -43,37 +34,18 @@ export function isNumericType(type: unknown): type is ESQLNumericLiteralType { ); } -export function isNumericDecimalType(type: unknown): type is ESQLDecimalLiteral { - return ( - typeof type === 'string' && - ESQL_NUMERIC_DECIMAL_TYPES.includes(type as (typeof ESQL_NUMERIC_DECIMAL_TYPES)[number]) - ); -} - /** * Compares two types, taking into account literal types * @TODO strengthen typing here (remove `string`) + * @TODO — clean up time duration and date period */ export const compareTypesWithLiterals = ( - a: ESQLLiteral['literalType'] | FunctionParameterType | string, - b: ESQLLiteral['literalType'] | FunctionParameterType | string + a: ESQLLiteral['literalType'] | FunctionParameterType | 'timeInterval' | string, + b: ESQLLiteral['literalType'] | FunctionParameterType | 'timeInterval' | string ) => { if (a === b) { return true; } - if (a === 'decimal') { - return isNumericDecimalType(b); - } - if (b === 'decimal') { - return isNumericDecimalType(a); - } - if (a === 'string') { - return isStringType(b); - } - if (b === 'string') { - return isStringType(a); - } - // In Elasticsearch function definitions, time_literal and time_duration are used // time_duration is seconds/min/hour interval // date_period is day/week/month/year interval diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts index 98d2da8d78cc0..0078e0fac119c 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts @@ -7,7 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { shouldBeQuotedSource } from './helpers'; +import { parse } from '@kbn/esql-ast'; +import { getExpressionType, shouldBeQuotedSource } from './helpers'; +import { SupportedDataType } from '../definitions/types'; +import { setTestFunctions } from './test_functions'; describe('shouldBeQuotedSource', () => { it('does not have to be quoted for sources with acceptable characters @-+$', () => { @@ -47,3 +50,295 @@ describe('shouldBeQuotedSource', () => { expect(shouldBeQuotedSource('index-[dd-mm]')).toBe(true); }); }); + +describe('getExpressionType', () => { + const getASTForExpression = (expression: string) => { + const { root } = parse(`FROM index | EVAL ${expression}`); + return root.commands[1].args[0]; + }; + + describe('literal expressions', () => { + const cases: Array<{ expression: string; expectedType: SupportedDataType }> = [ + { + expression: '1.0', + expectedType: 'double', + }, + { + expression: '1', + expectedType: 'integer', + }, + { + expression: 'true', + expectedType: 'boolean', + }, + { + expression: '"foobar"', + expectedType: 'keyword', + }, + { + expression: 'NULL', + expectedType: 'null', + }, + // TODO — consider whether we need to be worried about + // differentiating between time_duration, and date_period + // instead of just using time_literal + { + expression: '1 second', + expectedType: 'time_literal', + }, + { + expression: '1 day', + expectedType: 'time_literal', + }, + ]; + test.each(cases)('detects a literal of type $expectedType', ({ expression, expectedType }) => { + const ast = getASTForExpression(expression); + expect(getExpressionType(ast)).toBe(expectedType); + }); + }); + + describe('inline casting', () => { + const cases: Array<{ expression: string; expectedType: SupportedDataType }> = [ + { expectedType: 'boolean', expression: '"true"::bool' }, + { expectedType: 'boolean', expression: '"false"::boolean' }, + { expectedType: 'boolean', expression: '"false"::BooLEAN' }, + { expectedType: 'cartesian_point', expression: '""::cartesian_point' }, + { expectedType: 'cartesian_shape', expression: '""::cartesian_shape' }, + { expectedType: 'date_nanos', expression: '1::date_nanos' }, + { expectedType: 'date_period', expression: '1::date_period' }, + { expectedType: 'date', expression: '1::datetime' }, + { expectedType: 'double', expression: '1::double' }, + { expectedType: 'geo_point', expression: '""::geo_point' }, + { expectedType: 'geo_shape', expression: '""::geo_shape' }, + { expectedType: 'integer', expression: '1.2::int' }, + { expectedType: 'integer', expression: '1.2::integer' }, + { expectedType: 'ip', expression: '"123.12.12.2"::ip' }, + { expectedType: 'keyword', expression: '1::keyword' }, + { expectedType: 'long', expression: '1::long' }, + { expectedType: 'keyword', expression: '1::string' }, + { expectedType: 'keyword', expression: '1::text' }, + { expectedType: 'time_duration', expression: '1::time_duration' }, + { expectedType: 'unsigned_long', expression: '1::unsigned_long' }, + { expectedType: 'version', expression: '"1.2.3"::version' }, + { expectedType: 'version', expression: '"1.2.3"::VERSION' }, + ]; + test.each(cases)( + 'detects a casted literal of type $expectedType ($expression)', + ({ expression, expectedType }) => { + const ast = getASTForExpression(expression); + expect(getExpressionType(ast)).toBe(expectedType); + } + ); + }); + + describe('fields and variables', () => { + it('detects the type of fields and variables which exist', () => { + expect( + getExpressionType( + getASTForExpression('fieldName'), + new Map([ + [ + 'fieldName', + { + name: 'fieldName', + type: 'geo_shape', + }, + ], + ]), + new Map() + ) + ).toBe('geo_shape'); + + expect( + getExpressionType( + getASTForExpression('var0'), + new Map(), + new Map([ + [ + 'var0', + [ + { + name: 'var0', + type: 'long', + location: { min: 0, max: 0 }, + }, + ], + ], + ]) + ) + ).toBe('long'); + }); + + it('handles fields and variables which do not exist', () => { + expect(getExpressionType(getASTForExpression('fieldName'), new Map(), new Map())).toBe( + 'unknown' + ); + }); + }); + + describe('functions', () => { + beforeAll(() => { + setTestFunctions([ + { + type: 'eval', + name: 'test', + description: 'Test function', + supportedCommands: ['eval'], + signatures: [ + { params: [{ name: 'arg', type: 'keyword' }], returnType: 'keyword' }, + { params: [{ name: 'arg', type: 'double' }], returnType: 'double' }, + { + params: [ + { name: 'arg', type: 'double' }, + { name: 'arg', type: 'keyword' }, + ], + returnType: 'long', + }, + ], + }, + { + type: 'eval', + name: 'returns_keyword', + description: 'Test function', + supportedCommands: ['eval'], + signatures: [{ params: [], returnType: 'keyword' }], + }, + { + type: 'eval', + name: 'accepts_dates', + description: 'Test function', + supportedCommands: ['eval'], + signatures: [ + { + params: [ + { name: 'arg1', type: 'date' }, + { name: 'arg2', type: 'date_period' }, + ], + returnType: 'keyword', + }, + ], + }, + ]); + }); + afterAll(() => { + setTestFunctions([]); + }); + + it('detects the return type of a function', () => { + expect( + getExpressionType(getASTForExpression('returns_keyword()'), new Map(), new Map()) + ).toBe('keyword'); + }); + + it('selects the correct signature based on the arguments', () => { + expect(getExpressionType(getASTForExpression('test("foo")'), new Map(), new Map())).toBe( + 'keyword' + ); + expect(getExpressionType(getASTForExpression('test(1.)'), new Map(), new Map())).toBe( + 'double' + ); + expect(getExpressionType(getASTForExpression('test(1., "foo")'), new Map(), new Map())).toBe( + 'long' + ); + }); + + it('supports nested functions', () => { + expect( + getExpressionType( + getASTForExpression('test(1., test(test(test(returns_keyword()))))'), + new Map(), + new Map() + ) + ).toBe('long'); + }); + + it('supports functions with casted results', () => { + expect( + getExpressionType(getASTForExpression('test(1.)::keyword'), new Map(), new Map()) + ).toBe('keyword'); + }); + + it('handles nulls and string-date casting', () => { + expect(getExpressionType(getASTForExpression('test(NULL)'), new Map(), new Map())).toBe( + 'null' + ); + expect(getExpressionType(getASTForExpression('test(NULL, NULL)'), new Map(), new Map())).toBe( + 'null' + ); + expect( + getExpressionType(getASTForExpression('accepts_dates("", "")'), new Map(), new Map()) + ).toBe('keyword'); + }); + + it('deals with functions that do not exist', () => { + expect(getExpressionType(getASTForExpression('does_not_exist()'), new Map(), new Map())).toBe( + 'unknown' + ); + }); + + it('deals with bad function invocations', () => { + expect( + getExpressionType(getASTForExpression('test(1., "foo", "bar")'), new Map(), new Map()) + ).toBe('unknown'); + + expect(getExpressionType(getASTForExpression('test()'), new Map(), new Map())).toBe( + 'unknown' + ); + + expect(getExpressionType(getASTForExpression('test("foo", 1.)'), new Map(), new Map())).toBe( + 'unknown' + ); + }); + + it('deals with the CASE function', () => { + expect(getExpressionType(getASTForExpression('CASE(true, 1, 2)'), new Map(), new Map())).toBe( + 'integer' + ); + + expect( + getExpressionType(getASTForExpression('CASE(true, 1., true, 1., 2.)'), new Map(), new Map()) + ).toBe('double'); + + expect( + getExpressionType( + getASTForExpression('CASE(true, "", true, "", keywordField)'), + new Map([[`keywordField`, { name: 'keywordField', type: 'keyword' }]]), + new Map() + ) + ).toBe('keyword'); + }); + }); + + describe('lists', () => { + const cases: Array<{ expression: string; expectedType: SupportedDataType | 'unknown' }> = [ + { + expression: '["foo", "bar"]', + expectedType: 'keyword', + }, + { + expression: '[1, 2]', + expectedType: 'integer', + }, + { + expression: '[1., 2.]', + expectedType: 'double', + }, + { + expression: '[null, null, null]', + expectedType: 'null', + }, + { + expression: '[true, false]', + expectedType: 'boolean', + }, + ]; + + test.each(cases)( + 'reports the type of $expression as $expectedType', + ({ expression, expectedType }) => { + const ast = getASTForExpression(expression); + expect(getExpressionType(ast)).toBe(expectedType); + } + ); + }); +}); diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index e3e3da4277344..31c2c01a11404 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -43,10 +43,10 @@ import { FunctionParameterType, FunctionReturnType, ArrayType, + SupportedDataType, } from '../definitions/types'; import type { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; import { removeMarkerArgFromArgsList } from './context'; -import { compareTypesWithLiterals, isNumericDecimalType } from './esql_types'; import type { ReasonTypes } from './types'; import { DOUBLE_TICKS_REGEX, EDITOR_MARKER, SINGLE_BACKTICK } from './constants'; import type { EditorContext } from '../autocomplete/types'; @@ -225,27 +225,29 @@ export function getCommandOption(optionName: CommandOptionsDefinition['name']) { } function doesLiteralMatchParameterType(argType: FunctionParameterType, item: ESQLLiteral) { - if (item.literalType === 'null') { + if (item.literalType === argType) { return true; } - if (item.literalType === 'decimal' && isNumericDecimalType(argType)) { + if (item.literalType === 'null') { + // all parameters accept null, but this is not yet reflected + // in our function definitions so we let it through here return true; } - if (item.literalType === 'string' && (argType === 'text' || argType === 'keyword')) { + // some parameters accept string literals because of ES auto-casting + if ( + item.literalType === 'keyword' && + (argType === 'date' || + argType === 'date_period' || + argType === 'version' || + argType === 'ip' || + argType === 'boolean') + ) { return true; } - if (item.literalType !== 'string') { - if (argType === item.literalType) { - return true; - } - return false; - } - - // date-type parameters accept string literals because of ES auto-casting - return ['string', 'date', 'date_period'].includes(argType); + return false; } /** @@ -417,7 +419,7 @@ export function inKnownTimeInterval(item: ESQLTimeInterval): boolean { */ export function isValidLiteralOption(arg: ESQLLiteral, argDef: FunctionParameter) { return ( - arg.literalType === 'string' && + arg.literalType === 'keyword' && argDef.acceptedValues && !argDef.acceptedValues .map((option) => option.toLowerCase()) @@ -447,7 +449,7 @@ export function checkFunctionArgMatchesDefinition( if (isSupportedFunction(arg.name, parentCommand).supported) { const fnDef = buildFunctionLookup().get(arg.name)!; return fnDef.signatures.some( - (signature) => signature.returnType === 'any' || argType === signature.returnType + (signature) => signature.returnType === 'unknown' || argType === signature.returnType ); } } @@ -460,23 +462,15 @@ export function checkFunctionArgMatchesDefinition( if (!validHit) { return false; } - const wrappedTypes = Array.isArray(validHit.type) ? validHit.type : [validHit.type]; - // if final type is of type any make it pass for now - return wrappedTypes.some( - (ct) => - ['any', 'null'].includes(ct) || - argType === ct || - (ct === 'string' && ['text', 'keyword'].includes(argType as string)) - ); + const wrappedTypes: Array<(typeof validHit)['type']> = Array.isArray(validHit.type) + ? validHit.type + : [validHit.type]; + return wrappedTypes.some((ct) => ct === argType || ct === 'null' || ct === 'unknown'); } if (arg.type === 'inlineCast') { const lowerArgType = argType?.toLowerCase(); - const lowerArgCastType = arg.castType?.toLowerCase(); - return ( - compareTypesWithLiterals(lowerArgCastType, lowerArgType) || - // for valid shorthand casts like 321.12::int or "false"::bool - (['int', 'bool'].includes(lowerArgCastType) && argType.startsWith(lowerArgCastType)) - ); + const castedType = getExpressionType(arg); + return castedType === lowerArgType; } } @@ -725,3 +719,143 @@ export function correctQuerySyntax(_query: string, context: EditorContext) { return query; } + +/** + * Gets the signatures of a function that match the number of arguments + * provided in the AST. + */ +export function getSignaturesWithMatchingArity( + fnDef: FunctionDefinition, + astFunction: ESQLFunction +) { + return fnDef.signatures.filter((def) => { + if (def.minParams) { + return astFunction.args.length >= def.minParams; + } + return ( + astFunction.args.length >= def.params.filter(({ optional }) => !optional).length && + astFunction.args.length <= def.params.length + ); + }); +} + +/** + * Given a function signature, returns the parameter at the given position. + * + * Takes into account variadic functions (minParams), returning the last + * parameter if the position is greater than the number of parameters. + * + * @param signature + * @param position + * @returns + */ +export function getParamAtPosition( + { params, minParams }: FunctionDefinition['signatures'][number], + position: number +) { + return params.length > position ? params[position] : minParams ? params[params.length - 1] : null; +} + +/** + * Determines the type of the expression + */ +export function getExpressionType( + root: ESQLAstItem, + fields?: Map, + variables?: Map +): SupportedDataType | 'unknown' { + if (!isSingleItem(root)) { + if (root.length === 0) { + return 'unknown'; + } + return getExpressionType(root[0], fields, variables); + } + + if (isLiteralItem(root) && root.literalType !== 'param') { + return root.literalType; + } + + if (isTimeIntervalItem(root)) { + return 'time_literal'; + } + + // from https://github.com/elastic/elasticsearch/blob/122e7288200ee03e9087c98dff6cebbc94e774aa/docs/reference/esql/functions/kibana/inline_cast.json + if (isInlineCastItem(root)) { + switch (root.castType) { + case 'int': + return 'integer'; + case 'bool': + return 'boolean'; + case 'string': + return 'keyword'; + case 'text': + return 'keyword'; + case 'datetime': + return 'date'; + default: + return root.castType; + } + } + + if (isColumnItem(root) && fields && variables) { + const column = getColumnForASTNode(root, { fields, variables }); + if (!column) { + return 'unknown'; + } + return column.type; + } + + if (root.type === 'list') { + return getExpressionType(root.values[0], fields, variables); + } + + if (isFunctionItem(root)) { + const fnDefinition = getFunctionDefinition(root.name); + if (!fnDefinition) { + return 'unknown'; + } + + if (fnDefinition.name === 'case' && root.args.length) { + // The CASE function doesn't fit our system of function definitions + // and needs special handling. This is imperfect, but it's a start because + // at least we know that the final argument to case will never be a conditional + // expression, always a result expression. + // + // One problem with this is that if a false case is not provided, the return type + // will be null, which we aren't detecting. But this is ok because we consider + // variables and fields to be nullable anyways and account for that during validation. + return getExpressionType(root.args[root.args.length - 1], fields, variables); + } + + const signaturesWithCorrectArity = getSignaturesWithMatchingArity(fnDefinition, root); + + if (!signaturesWithCorrectArity.length) { + return 'unknown'; + } + + const argTypes = root.args.map((arg) => getExpressionType(arg, fields, variables)); + + // When functions are passed null for any argument, they generally return null + // This is a special case that is not reflected in our function definitions + if (argTypes.some((argType) => argType === 'null')) return 'null'; + + const matchingSignature = signaturesWithCorrectArity.find((signature) => { + return argTypes.every((argType, i) => { + const param = getParamAtPosition(signature, i); + return ( + param && + (param.type === argType || + (argType === 'keyword' && ['date', 'date_period'].includes(param.type))) + ); + }); + }); + + if (!matchingSignature) { + return 'unknown'; + } + + return matchingSignature.returnType === 'any' ? 'unknown' : matchingSignature.returnType; + } + + return 'unknown'; +} diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/variables.ts b/packages/kbn-esql-validation-autocomplete/src/shared/variables.ts index 29656d2f581ed..c2de407264a99 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/variables.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/variables.ts @@ -11,7 +11,7 @@ import type { ESQLAst, ESQLAstItem, ESQLCommand, ESQLFunction } from '@kbn/esql- import { Visitor } from '@kbn/esql-ast/src/visitor'; import type { ESQLVariable, ESQLRealField } from '../validation/types'; import { EDITOR_MARKER } from './constants'; -import { isColumnItem, isFunctionItem, getFunctionDefinition } from './helpers'; +import { isColumnItem, isFunctionItem, getExpressionType } from './helpers'; function addToVariableOccurrences(variables: Map, instance: ESQLVariable) { if (!variables.has(instance.name)) { @@ -43,62 +43,6 @@ function addToVariables( } } -/** - * Determines the type of the expression - * - * TODO - this function needs a lot of work. For example, it needs to find the best-matching function signature - * which it isn't currently doing. See https://github.com/elastic/kibana/issues/195682 - */ -function getExpressionType( - root: ESQLAstItem, - fields: Map, - variables: Map -): string { - const fallback = 'double'; - - if (Array.isArray(root) || !root) { - return fallback; - } - if (root.type === 'literal') { - return root.literalType; - } - if (root.type === 'inlineCast') { - if (root.castType === 'int') { - return 'integer'; - } - if (root.castType === 'bool') { - return 'boolean'; - } - return root.castType; - } - if (isColumnItem(root)) { - const field = fields.get(root.parts.join('.')); - if (field) { - return field.type; - } - const variable = variables.get(root.parts.join('.')); - if (variable) { - return variable[0].type; - } - } - if (isFunctionItem(root)) { - const fnDefinition = getFunctionDefinition(root.name); - return fnDefinition?.signatures[0].returnType ?? fallback; - } - return fallback; -} - -function getAssignRightHandSideType( - item: ESQLAstItem, - fields: Map, - variables: Map -) { - if (Array.isArray(item)) { - const firstArg = item[0]; - return getExpressionType(firstArg, fields, variables); - } -} - export function excludeVariablesFromCurrentCommand( commands: ESQLCommand[], currentCommand: ESQLCommand, @@ -122,14 +66,10 @@ function addVariableFromAssignment( fields: Map ) { if (isColumnItem(assignOperation.args[0])) { - const rightHandSideArgType = getAssignRightHandSideType( - assignOperation.args[1], - fields, - variables - ); + const rightHandSideArgType = getExpressionType(assignOperation.args[1], fields, variables); addToVariableOccurrences(variables, { name: assignOperation.args[0].parts.join('.'), - type: rightHandSideArgType as string /* fallback to number */, + type: rightHandSideArgType /* fallback to number */, location: assignOperation.args[0].location, }); } @@ -138,14 +78,15 @@ function addVariableFromAssignment( function addVariableFromExpression( expressionOperation: ESQLFunction, queryString: string, - variables: Map + variables: Map, + fields: Map ) { if (!expressionOperation.text.includes(EDITOR_MARKER)) { const expressionText = queryString.substring( expressionOperation.location.min, expressionOperation.location.max + 1 ); - const expressionType = 'double'; // TODO - use getExpressionType once it actually works + const expressionType = getExpressionType(expressionOperation, fields, variables); addToVariableOccurrences(variables, { name: expressionText, type: expressionType, @@ -174,7 +115,7 @@ export function collectVariables( if (ctx.node.name === '=') { addVariableFromAssignment(ctx.node, variables, fields); } else { - addVariableFromExpression(ctx.node, queryString, variables); + addVariableFromExpression(ctx.node, queryString, variables, fields); } }) .on('visitCommandOption', (ctx) => { diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts index 9cf211315757e..a3934c1a35627 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/functions.test.ts @@ -100,12 +100,12 @@ describe('function validation', () => { // straight call await expectErrors('FROM a_index | EVAL TEST(1.1)', [ - 'Argument of [test] must be [integer], found value [1.1] type [decimal]', + 'Argument of [test] must be [integer], found value [1.1] type [double]', ]); // assignment await expectErrors('FROM a_index | EVAL var = TEST(1.1)', [ - 'Argument of [test] must be [integer], found value [1.1] type [decimal]', + 'Argument of [test] must be [integer], found value [1.1] type [double]', ]); // nested function @@ -115,7 +115,7 @@ describe('function validation', () => { // inline cast await expectErrors('FROM a_index | EVAL TEST(1::DOUBLE)', [ - 'Argument of [test] must be [integer], found value [1::DOUBLE] type [DOUBLE]', + 'Argument of [test] must be [integer], found value [1::DOUBLE] type [double]', ]); // field @@ -125,13 +125,13 @@ describe('function validation', () => { // variables await expectErrors('FROM a_index | EVAL var1 = 1. | EVAL TEST(var1)', [ - 'Argument of [test] must be [integer], found value [var1] type [decimal]', + 'Argument of [test] must be [integer], found value [var1] type [double]', ]); // multiple instances await expectErrors('FROM a_index | EVAL TEST(1.1) | EVAL TEST(1.1)', [ - 'Argument of [test] must be [integer], found value [1.1] type [decimal]', - 'Argument of [test] must be [integer], found value [1.1] type [decimal]', + 'Argument of [test] must be [integer], found value [1.1] type [double]', + 'Argument of [test] must be [integer], found value [1.1] type [double]', ]); }); @@ -190,7 +190,7 @@ describe('function validation', () => { await expectErrors('ROW "a" IN ("a", "b", "c")', []); await expectErrors('ROW "a" IN (1, "b", "c")', [ - 'Argument of [in] must be [keyword[]], found value [(1, "b", "c")] type [(integer, string, string)]', + 'Argument of [in] must be [keyword[]], found value [(1, "b", "c")] type [(integer, keyword, keyword)]', ]); }); }); @@ -238,9 +238,9 @@ describe('function validation', () => { // double, double, double await expectErrors('FROM a_index | EVAL TEST(1., 1., 1.)', []); await expectErrors('FROM a_index | EVAL TEST("", "", "")', [ - 'Argument of [test] must be [double], found value [""] type [string]', - 'Argument of [test] must be [double], found value [""] type [string]', - 'Argument of [test] must be [double], found value [""] type [string]', + 'Argument of [test] must be [double], found value [""] type [keyword]', + 'Argument of [test] must be [double], found value [""] type [keyword]', + 'Argument of [test] must be [double], found value [""] type [keyword]', ]); // int, int @@ -260,7 +260,7 @@ describe('function validation', () => { // date await expectErrors('FROM a_index | EVAL TEST(NOW())', []); await expectErrors('FROM a_index | EVAL TEST(1.)', [ - 'Argument of [test] must be [date], found value [1] type [decimal]', + 'Argument of [test] must be [date], found value [1.] type [double]', ]); }); }); @@ -721,5 +721,7 @@ describe('function validation', () => { // 'No nested aggregation functions.', // ]); }); + + // @TODO — test function aliases }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index a646c0323a76f..51ada18f02252 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -6654,14 +6654,14 @@ { "query": "from a_index | eval 1 * \"1\"", "error": [ - "Argument of [*] must be [double], found value [\"1\"] type [string]" + "Argument of [*] must be [double], found value [\"1\"] type [keyword]" ], "warning": [] }, { "query": "from a_index | eval \"1\" * 1", "error": [ - "Argument of [*] must be [double], found value [\"1\"] type [string]" + "Argument of [*] must be [double], found value [\"1\"] type [keyword]" ], "warning": [] }, @@ -6691,14 +6691,14 @@ { "query": "from a_index | eval 1 / \"1\"", "error": [ - "Argument of [/] must be [double], found value [\"1\"] type [string]" + "Argument of [/] must be [double], found value [\"1\"] type [keyword]" ], "warning": [] }, { "query": "from a_index | eval \"1\" / 1", "error": [ - "Argument of [/] must be [double], found value [\"1\"] type [string]" + "Argument of [/] must be [double], found value [\"1\"] type [keyword]" ], "warning": [] }, @@ -6728,14 +6728,14 @@ { "query": "from a_index | eval 1 % \"1\"", "error": [ - "Argument of [%] must be [double], found value [\"1\"] type [string]" + "Argument of [%] must be [double], found value [\"1\"] type [keyword]" ], "warning": [] }, { "query": "from a_index | eval \"1\" % 1", "error": [ - "Argument of [%] must be [double], found value [\"1\"] type [string]" + "Argument of [%] must be [double], found value [\"1\"] type [keyword]" ], "warning": [] }, @@ -9513,7 +9513,7 @@ "query": "from a_index | eval doubleField = \"5\"", "error": [], "warning": [ - "Column [doubleField] of type double has been overwritten as new type: string" + "Column [doubleField] of type double has been overwritten as new type: keyword" ] }, { @@ -9655,19 +9655,19 @@ "warning": [] }, { - "query": "from a_index | eval true AND \"false\"::boolean", + "query": "from a_index | eval true AND 0::boolean", "error": [], "warning": [] }, { - "query": "from a_index | eval true AND \"false\"::bool", + "query": "from a_index | eval true AND 0::bool", "error": [], "warning": [] }, { - "query": "from a_index | eval true AND \"false\"", + "query": "from a_index | eval true AND 0", "error": [ - "Argument of [and] must be [boolean], found value [\"false\"] type [string]" + "Argument of [and] must be [boolean], found value [0] type [integer]" ], "warning": [] }, diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/types.ts b/packages/kbn-esql-validation-autocomplete/src/validation/types.ts index 99ce0f8ac5196..7aac9f16ad032 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/types.ts @@ -8,12 +8,15 @@ */ import type { ESQLMessage, ESQLLocation } from '@kbn/esql-ast'; -import { FieldType } from '../definitions/types'; +import { FieldType, SupportedDataType } from '../definitions/types'; import type { EditorError } from '../types'; export interface ESQLVariable { name: string; - type: string; + // invalid expressions produce columns of type "unknown" + // also, there are some cases where we can't yet infer the type of + // a valid expression as with `CASE` which can return union types + type: SupportedDataType | 'unknown'; location: ESQLLocation; } diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index dd04f0e506fe8..5e636a941a86c 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -1129,13 +1129,13 @@ describe('validation logic', () => { `from a_index | eval 1 ${op} "1"`, ['+', '-'].includes(op) ? [`Argument of [${op}] must be [date_period], found value [1] type [integer]`] - : [`Argument of [${op}] must be [double], found value [\"1\"] type [string]`] + : [`Argument of [${op}] must be [double], found value [\"1\"] type [keyword]`] ); testErrorsAndWarnings( `from a_index | eval "1" ${op} 1`, ['+', '-'].includes(op) ? [`Argument of [${op}] must be [date_period], found value [1] type [integer]`] - : [`Argument of [${op}] must be [double], found value [\"1\"] type [string]`] + : [`Argument of [${op}] must be [double], found value [\"1\"] type [keyword]`] ); // TODO: enable when https://github.com/elastic/elasticsearch/issues/108432 is complete // testErrorsAndWarnings(`from a_index | eval "2022" ${op} 1 day`, []); @@ -1478,7 +1478,7 @@ describe('validation logic', () => { testErrorsAndWarnings( 'from a_index | eval doubleField = "5"', [], - ['Column [doubleField] of type double has been overwritten as new type: string'] + ['Column [doubleField] of type double has been overwritten as new type: keyword'] ); }); @@ -1674,11 +1674,11 @@ describe('validation logic', () => { testErrorsAndWarnings('from a_index | eval TRIM(23::text)', []); testErrorsAndWarnings('from a_index | eval TRIM(23::keyword)', []); - testErrorsAndWarnings('from a_index | eval true AND "false"::boolean', []); - testErrorsAndWarnings('from a_index | eval true AND "false"::bool', []); - testErrorsAndWarnings('from a_index | eval true AND "false"', [ + testErrorsAndWarnings('from a_index | eval true AND 0::boolean', []); + testErrorsAndWarnings('from a_index | eval true AND 0::bool', []); + testErrorsAndWarnings('from a_index | eval true AND 0', [ // just a counter-case to make sure the previous tests are meaningful - 'Argument of [and] must be [boolean], found value ["false"] type [string]', + 'Argument of [and] must be [boolean], found value [0] type [integer]', ]); // enforces strings for cartesian_point conversion diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts index 23508eeedd234..9605da8460eed 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -26,7 +26,6 @@ import { CommandModeDefinition, CommandOptionsDefinition, FunctionParameter, - FunctionDefinition, } from '../definitions/types'; import { areFieldAndVariableTypesCompatible, @@ -54,6 +53,7 @@ import { isAggFunction, getQuotedColumnName, isInlineCastItem, + getSignaturesWithMatchingArity, } from '../shared/helpers'; import { collectVariables } from '../shared/variables'; import { getMessageFromId, errors } from './errors'; @@ -74,7 +74,7 @@ import { retrieveFieldsFromStringSources, } from './resources'; import { collapseWrongArgumentTypeMessages, getMaxMinNumberOfParams } from './helpers'; -import { getParamAtPosition } from '../autocomplete/helper'; +import { getParamAtPosition } from '../shared/helpers'; import { METADATA_FIELDS } from '../shared/constants'; import { compareTypesWithLiterals } from '../shared/esql_types'; @@ -88,7 +88,7 @@ function validateFunctionLiteralArg( const messages: ESQLMessage[] = []; if (isLiteralItem(actualArg)) { if ( - actualArg.literalType === 'string' && + actualArg.literalType === 'keyword' && argDef.acceptedValues && isValidLiteralOption(actualArg, argDef) ) { @@ -112,7 +112,7 @@ function validateFunctionLiteralArg( values: { name: astFunction.name, argType: argDef.type as string, - value: typeof actualArg.value === 'number' ? actualArg.value : String(actualArg.value), + value: actualArg.text, givenType: actualArg.literalType, }, locations: actualArg.location, @@ -309,21 +309,6 @@ function validateFunctionColumnArg( return messages; } -function extractCompatibleSignaturesForFunction( - fnDef: FunctionDefinition, - astFunction: ESQLFunction -) { - return fnDef.signatures.filter((def) => { - if (def.minParams) { - return astFunction.args.length >= def.minParams; - } - return ( - astFunction.args.length >= def.params.filter(({ optional }) => !optional).length && - astFunction.args.length <= def.params.length - ); - }); -} - function removeInlineCasts(arg: ESQLAstItem): ESQLAstItem { if (isInlineCastItem(arg)) { return removeInlineCasts(arg.value); @@ -376,7 +361,7 @@ function validateFunction( return messages; } } - const matchingSignatures = extractCompatibleSignaturesForFunction(fnDefinition, astFunction); + const matchingSignatures = getSignaturesWithMatchingArity(fnDefinition, astFunction); if (!matchingSignatures.length) { const { max, min } = getMaxMinNumberOfParams(fnDefinition); if (max === min) { diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts index 5723dca7b339b..efc86f85213c0 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/saml_auth_provider.ts @@ -33,7 +33,7 @@ export interface KibanaRoleDescriptors { } const throwIfRoleNotSet = (role: string, customRole: string, roleDescriptors: Map) => { - if (role === customRole && !roleDescriptors.has(customRole)) { + if (role === customRole && !roleDescriptors.get(customRole)) { throw new Error( `Set privileges for '${customRole}' using 'samlAuth.setCustomRole' before authentication.` ); @@ -179,7 +179,7 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { if (!isCustomRoleEnabled) { throw new Error(`Custom roles are not supported for the current deployment`); } - log.debug(`Updating role ${CUSTOM_ROLE}`); + log.debug(`Updating role '${CUSTOM_ROLE}'`); const adminCookieHeader = await getAdminCredentials(); const customRoleDescriptors = { @@ -199,6 +199,28 @@ export function SamlAuthProvider({ getService }: FtrProviderContext) { supportedRoleDescriptors.set(CUSTOM_ROLE, customRoleDescriptors); }, + async deleteCustomRole() { + if (!isCustomRoleEnabled) { + throw new Error(`Custom roles are not supported for the current deployment`); + } + + if (supportedRoleDescriptors.get(CUSTOM_ROLE)) { + log.debug(`Deleting role '${CUSTOM_ROLE}'`); + const adminCookieHeader = await getAdminCredentials(); + + // Resetting descriptors for the custom role, even if role deletion fails + supportedRoleDescriptors.set(CUSTOM_ROLE, null); + log.debug(`'${CUSTOM_ROLE}' descriptors were reset`); + + const { status } = await supertestWithoutAuth + .delete(`/api/security/role/${CUSTOM_ROLE}`) + .set(INTERNAL_REQUEST_HEADERS) + .set(adminCookieHeader); + + expect(status).to.be(204); + } + }, + getCommonRequestHeader() { return COMMON_REQUEST_HEADERS; }, diff --git a/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts b/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts index 25038a3cfa17b..16c2dc9cfa844 100644 --- a/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts +++ b/packages/kbn-ftr-common-functional-services/services/saml_auth/serverless/auth_provider.ts @@ -34,8 +34,18 @@ const getDefaultServerlessRole = (projectType: string) => { } }; +const isRoleManagementExplicitlyEnabled = (args: string[]): boolean => { + const roleManagementArg = args.find((arg) => + arg.startsWith('--xpack.security.roleManagementEnabled=') + ); + + // Return true if the value is explicitly set to 'true', otherwise false + return roleManagementArg?.split('=')[1] === 'true' || false; +}; + export class ServerlessAuthProvider implements AuthProvider { private readonly projectType: string; + private readonly roleManagementEnabled: boolean; private readonly rolesDefinitionPath: string; constructor(config: Config) { @@ -45,6 +55,10 @@ export class ServerlessAuthProvider implements AuthProvider { return acc + (match ? match[1] : ''); }, '') as ServerlessProjectType; + // Indicates whether role management was explicitly enabled using + // the `--xpack.security.roleManagementEnabled=true` flag. + this.roleManagementEnabled = isRoleManagementExplicitlyEnabled(kbnServerArgs); + if (!isServerlessProjectType(this.projectType)) { throw new Error(`Unsupported serverless projectType: ${this.projectType}`); } @@ -70,7 +84,9 @@ export class ServerlessAuthProvider implements AuthProvider { } isCustomRoleEnabled() { - return projectTypesWithCustomRolesEnabled.includes(this.projectType); + return ( + projectTypesWithCustomRolesEnabled.includes(this.projectType) || this.roleManagementEnabled + ); } getCustomRole() { diff --git a/packages/serverless/settings/observability_project/index.ts b/packages/serverless/settings/observability_project/index.ts index f8bb8dbe12542..44f30e4320463 100644 --- a/packages/serverless/settings/observability_project/index.ts +++ b/packages/serverless/settings/observability_project/index.ts @@ -37,5 +37,4 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [ settings.OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING, settings.OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN, settings.OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID, - settings.OBSERVABILITY_SEARCH_EXCLUDED_DATA_TIERS, ]; diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 406f6e4de5af0..2eaa14bf24eda 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -123,7 +123,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-agent-policies": "5e95e539826a40ad08fd0c1d161da0a4d86ffc6d", "ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d", "ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91", - "ingest-package-policies": "dc2af447c335215be2d6f7b7b8d437d05d6a1188", + "ingest-package-policies": "53a94064674835fdb35e5186233bcd7052eabd22", "ingest_manager_settings": "111a616eb72627c002029c19feb9e6c439a10505", "inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83", "kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad", diff --git a/src/dev/performance/run_performance_cli.ts b/src/dev/performance/run_performance_cli.ts index df6020ba62a34..fd0f4094124ab 100644 --- a/src/dev/performance/run_performance_cli.ts +++ b/src/dev/performance/run_performance_cli.ts @@ -47,6 +47,7 @@ const journeyTargetGroups: JourneyTargetGroups = { maps: ['ecommerce_dashboard_map_only'], ml: ['aiops_log_rate_analysis', 'many_fields_transform', 'tsdb_logs_data_visualizer'], esql: ['many_fields_discover_esql', 'web_logs_dashboard_esql'], + http2: ['data_stress_test_lens_http2', 'ecommerce_dashboard_http2'], }; const readFilesRecursively = (dir: string, callback: Function) => { diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx index 86940d52a81b3..e9f51a808b335 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx @@ -231,7 +231,7 @@ export class SavedObjectFinderUi extends React.Component< name: i18n.translate('savedObjectsFinder.typeName', { defaultMessage: 'Type', }), - width: '50px', + width: '70px', align: 'center', description: i18n.translate('savedObjectsFinder.typeDescription', { defaultMessage: 'Type of the saved object', diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/cumulative_sum.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/cumulative_sum.ts index 07ed7eceb9930..3b470da64c5c2 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/cumulative_sum.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/cumulative_sum.ts @@ -33,7 +33,7 @@ export const convertToCumulativeSumColumns = ( // lens supports cumulative sum for count and sum as quick function // and everything else as formula if (subFunctionMetric.type !== 'count' && pipelineAgg.name !== 'sum') { - const metaValue = Number(meta?.replace(']', '')); + const metaValue = Number(meta?.replace(/\]/g, '')); formula = getPipelineSeriesFormula(metric, metrics, subFunctionMetric, { metaValue, reducedTimeRange, diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts index cd0a3a62a8dd5..a1a42c12a64fa 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/formula.ts @@ -66,7 +66,7 @@ const convertFormulaScriptForPercentileAggs = ( ) => { variables.forEach((variable) => { const [_, meta] = variable?.field?.split('[') ?? []; - const metaValue = Number(meta?.replace(']', '')); + const metaValue = Number(meta?.replace(/\]/g, '')); if (!metaValue) { return; } @@ -163,7 +163,7 @@ export const convertOtherAggsToFormulaColumn = ( const metric = metrics[metrics.length - 1]; const [fieldId, meta] = metric?.field?.split('[') ?? []; const subFunctionMetric = metrics.find(({ id }) => id === fieldId); - const metaValue = meta ? Number(meta?.replace(']', '')) : undefined; + const metaValue = meta ? Number(meta?.replace(/\]/g, '')) : undefined; if (!subFunctionMetric) { return null; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/parent_pipeline.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/parent_pipeline.ts index 5ac5701eef6c7..e617ef70c5da2 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/parent_pipeline.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/convert/parent_pipeline.ts @@ -235,7 +235,7 @@ const convertMovingAvgOrDerivativeToColumns = ( if (!pipelineAgg) { return null; } - const metaValue = Number(meta?.replace(']', '')); + const metaValue = Number(meta?.replace(/\]/g, '')); const subMetricField = subFunctionMetric.field; const [nestedFieldId, _] = subMetricField?.split('[') ?? []; // support nested aggs with formula diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts index 22cd37255b59e..813f28d4b6924 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts @@ -14,7 +14,7 @@ import { addAdditionalArgs } from '.'; import { AdditionalArgs } from '../../types'; const escapeQuotes = (str: string) => { - return str?.replace(/'/g, "\\'"); + return str?.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }; const constructFilterRationFormula = ( diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts index da5e9e8cffb35..14e1b66df1e26 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts @@ -113,7 +113,7 @@ export const getFormulaEquivalent = ( } return getPipelineSeriesFormula(currentMetric, metrics, subFunctionMetric, { - metaValue: nestedMetaValue ? Number(nestedMetaValue?.replace(']', '')) : undefined, + metaValue: nestedMetaValue ? Number(nestedMetaValue?.replace(/\]/g, '')) : undefined, reducedTimeRange, timeShift, }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx index 6e4a114c14256..c2ec745cc5c64 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx @@ -24,7 +24,7 @@ export const ANONYMIZATION_TABLE_SESSION_STORAGE_KEY = 'anonymizationTable'; export const DEFAULT_LATEST_ALERTS = 100; /** The default maximum number of alerts to be sent as context when generating Attack discoveries */ -export const DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS = 200; +export const DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS = 100; export const DEFAULT_KNOWLEDGE_BASE_SETTINGS: KnowledgeBaseConfig = { latestAlerts: DEFAULT_LATEST_ALERTS, diff --git a/x-pack/packages/ml/data_grid/hooks/use_data_grid.tsx b/x-pack/packages/ml/data_grid/hooks/use_data_grid.tsx index dc87c68cc9b7c..f5ddb45642985 100644 --- a/x-pack/packages/ml/data_grid/hooks/use_data_grid.tsx +++ b/x-pack/packages/ml/data_grid/hooks/use_data_grid.tsx @@ -14,7 +14,7 @@ import { ES_CLIENT_TOTAL_HITS_RELATION } from '@kbn/ml-query-utils'; import { INDEX_STATUS } from '../lib/common'; import type { ChartData } from '../lib/field_histograms'; import { ColumnChart } from '../components/column_chart'; -import { COLUMN_CHART_DEFAULT_VISIBILITY_ROWS_THRESHOLD, INIT_MAX_COLUMNS } from '../lib/common'; +import { MAX_ROW_COUNT, INIT_MAX_COLUMNS } from '../lib/common'; import type { ChartsVisible, ColumnId, @@ -62,6 +62,11 @@ export const useDataGrid = ( const { rowCount, rowCountRelation } = rowCountInfo; + const setLimitedRowCountInfo = useCallback((info: RowCountInfo) => { + const limitedRowCount = Math.min(info.rowCount, MAX_ROW_COUNT); + setRowCountInfo({ rowCount: limitedRowCount, rowCountRelation: info.rowCountRelation }); + }, []); + const toggleChartVisibility = () => { if (chartsVisible !== undefined) { setChartsVisible(!chartsVisible); @@ -161,10 +166,7 @@ export const useDataGrid = ( // we decide whether to show or hide the charts by default. useEffect(() => { if (chartsVisible === undefined && rowCount > 0 && rowCountRelation !== undefined) { - setChartsVisible( - rowCount <= COLUMN_CHART_DEFAULT_VISIBILITY_ROWS_THRESHOLD && - rowCountRelation !== ES_CLIENT_TOTAL_HITS_RELATION.GTE - ); + setChartsVisible(rowCountRelation !== ES_CLIENT_TOTAL_HITS_RELATION.GTE); } }, [chartsVisible, rowCount, rowCountRelation]); @@ -189,7 +191,7 @@ export const useDataGrid = ( setErrorMessage, setNoDataMessage, setPagination, - setRowCountInfo, + setRowCountInfo: setLimitedRowCountInfo, setSortingColumns, setStatus, setTableItems, diff --git a/x-pack/packages/ml/data_grid/lib/common.ts b/x-pack/packages/ml/data_grid/lib/common.ts index 82c6cfa618174..bb20c8465dd4a 100644 --- a/x-pack/packages/ml/data_grid/lib/common.ts +++ b/x-pack/packages/ml/data_grid/lib/common.ts @@ -42,9 +42,9 @@ import type { DataGridItem, IndexPagination, RenderCellValue } from './types'; export const INIT_MAX_COLUMNS = 10; /** - * The default threshold value for the number of rows at which the column chart visibility is set to true. + * The default maximum row count value, set to 10000 due to ES limitations. */ -export const COLUMN_CHART_DEFAULT_VISIBILITY_ROWS_THRESHOLD = 10000; +export const MAX_ROW_COUNT = 10000; /** * Enum for index status diff --git a/x-pack/packages/ml/data_grid/lib/types.ts b/x-pack/packages/ml/data_grid/lib/types.ts index 4475d8572bcc8..a652e92d4c522 100644 --- a/x-pack/packages/ml/data_grid/lib/types.ts +++ b/x-pack/packages/ml/data_grid/lib/types.ts @@ -250,7 +250,7 @@ export interface UseDataGridReturnType { /** * Setter function for the row count info. */ - setRowCountInfo: Dispatch>; + setRowCountInfo: (info: RowCountInfo) => void; /** * Setter function for the sorting columns. */ diff --git a/x-pack/packages/security-solution/features/src/assistant/index.ts b/x-pack/packages/security-solution/features/src/assistant/index.ts index eaca4b4913ee2..ea0658d795455 100644 --- a/x-pack/packages/security-solution/features/src/assistant/index.ts +++ b/x-pack/packages/security-solution/features/src/assistant/index.ts @@ -9,11 +9,13 @@ import type { ProductFeatureParams } from '../types'; import { getAssistantBaseKibanaFeature } from './kibana_features'; import { getAssistantBaseKibanaSubFeatureIds, - assistantSubFeaturesMap, + getAssistantSubFeaturesMap, } from './kibana_sub_features'; -export const getAssistantFeature = (): ProductFeatureParams => ({ +export const getAssistantFeature = ( + experimentalFeatures: Record +): ProductFeatureParams => ({ baseKibanaFeature: getAssistantBaseKibanaFeature(), baseKibanaSubFeatureIds: getAssistantBaseKibanaSubFeatureIds(), - subFeaturesMap: assistantSubFeaturesMap, + subFeaturesMap: getAssistantSubFeaturesMap(experimentalFeatures), }); diff --git a/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts index d116aa36d21f0..dbf9505193ecf 100644 --- a/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/assistant/kibana_sub_features.ts @@ -102,9 +102,28 @@ export const getAssistantBaseKibanaSubFeatureIds = (): AssistantSubFeatureId[] = * Defines all the Security Assistant subFeatures available. * The order of the subFeatures is the order they will be displayed */ -export const assistantSubFeaturesMap = Object.freeze( - new Map([ +export const getAssistantSubFeaturesMap = ( + experimentalFeatures: Record +): Map => { + const assistantSubFeaturesList: Array<[AssistantSubFeatureId, SubFeatureConfig]> = [ [AssistantSubFeatureId.updateAnonymization, updateAnonymizationSubFeature], - [AssistantSubFeatureId.manageGlobalKnowledgeBase, manageGlobalKnowledgeBaseSubFeature], - ]) -); + ]; + + // Use the following code to add feature based on feature flag + // if (experimentalFeatures.featureFlagName) { + // assistantSubFeaturesList.push([AssistantSubFeatureId.featureId, featureSubFeature]); + // } + + if (experimentalFeatures.assistantKnowledgeBaseByDefault) { + assistantSubFeaturesList.push([ + AssistantSubFeatureId.manageGlobalKnowledgeBase, + manageGlobalKnowledgeBaseSubFeature, + ]); + } + + const assistantSubFeaturesMap = new Map( + assistantSubFeaturesList + ); + + return Object.freeze(assistantSubFeaturesMap); +}; diff --git a/x-pack/packages/security/plugin_types_public/index.ts b/x-pack/packages/security/plugin_types_public/index.ts index a48511441382a..fc8829ad8a5f8 100644 --- a/x-pack/packages/security/plugin_types_public/index.ts +++ b/x-pack/packages/security/plugin_types_public/index.ts @@ -24,3 +24,4 @@ export type { } from './src/roles'; export { PrivilegesAPIClientPublicContract } from './src/privileges'; export type { PrivilegesAPIClientGetAllArgs } from './src/privileges'; +export type { SecurityLicense } from './src/license'; diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/monitoring_config.ts b/x-pack/packages/security/plugin_types_public/src/license/index.ts similarity index 57% rename from x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/monitoring_config.ts rename to x-pack/packages/security/plugin_types_public/src/license/index.ts index 675dde6f25a4b..0c1ec0431c10a 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/monitoring_config.ts +++ b/x-pack/packages/security/plugin_types_public/src/license/index.ts @@ -5,13 +5,6 @@ * 2.0. */ -export const cluster = ['monitor']; +import type { SecurityPluginSetup } from '../plugin'; -export const privileges = ['auto_configure', 'create_doc']; - -export const indices = [ - { - names: ['logs-*-*', 'metrics-*-*'], - privileges, - }, -]; +export type SecurityLicense = SecurityPluginSetup['license']; diff --git a/x-pack/performance/configs/http2_config.ts b/x-pack/performance/configs/http2_config.ts new file mode 100644 index 0000000000000..d9d06b7c15ada --- /dev/null +++ b/x-pack/performance/configs/http2_config.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; +import { configureHTTP2 } from '@kbn/test-suites-src/common/configure_http2'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile( + require.resolve('@kbn/test-suites-xpack/functional/config.base') + ); + + return configureHTTP2({ + ...xpackFunctionalConfig.getAll(), + }); +} diff --git a/x-pack/performance/journeys_e2e/data_stress_test_lens_http2.ts b/x-pack/performance/journeys_e2e/data_stress_test_lens_http2.ts new file mode 100644 index 0000000000000..9f02fe7ba874a --- /dev/null +++ b/x-pack/performance/journeys_e2e/data_stress_test_lens_http2.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Journey } from '@kbn/journeys'; + +export const journey = new Journey({ + kbnArchives: ['test/functional/fixtures/kbn_archiver/stress_test'], + esArchives: ['test/functional/fixtures/es_archiver/stress_test'], + ftrConfigPath: 'x-pack/performance/configs/http2_config.ts', +}).step('Go to dashboard', async ({ page, kbnUrl, kibanaServer, kibanaPage }) => { + await kibanaServer.uiSettings.update({ 'histogram:maxBars': 100 }); + await page.goto(kbnUrl.get(`/app/dashboards#/view/92b143a0-2e9c-11ed-b1b6-a504560b392c`)); + await kibanaPage.waitForVisualizations({ count: 1 }); +}); diff --git a/x-pack/performance/journeys_e2e/ecommerce_dashboard_http2.ts b/x-pack/performance/journeys_e2e/ecommerce_dashboard_http2.ts new file mode 100644 index 0000000000000..57e11d461c00a --- /dev/null +++ b/x-pack/performance/journeys_e2e/ecommerce_dashboard_http2.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Journey } from '@kbn/journeys'; +import { subj } from '@kbn/test-subj-selector'; + +export const journey = new Journey({ + esArchives: ['x-pack/performance/es_archives/sample_data_ecommerce'], + kbnArchives: ['x-pack/performance/kbn_archives/ecommerce_no_map_dashboard'], + ftrConfigPath: 'x-pack/performance/configs/http2_config.ts', +}) + + .step('Go to Dashboards Page', async ({ page, kbnUrl, kibanaPage }) => { + await page.goto(kbnUrl.get(`/app/dashboards`)); + await kibanaPage.waitForListViewTable(); + }) + + .step('Go to Ecommerce Dashboard', async ({ page, kibanaPage }) => { + await page.click(subj('dashboardListingTitleLink-[eCommerce]-Revenue-Dashboard')); + await kibanaPage.waitForVisualizations({ count: 13 }); + }); diff --git a/x-pack/performance/tsconfig.json b/x-pack/performance/tsconfig.json index 5c00a3b2895d8..6718cd64c9640 100644 --- a/x-pack/performance/tsconfig.json +++ b/x-pack/performance/tsconfig.json @@ -22,5 +22,6 @@ "@kbn/expect", "@kbn/dev-utils", "@kbn/apm-synthtrace-client", + "@kbn/test-suites-src", ] } diff --git a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx index 1ba3f0fe3f454..7554716c59492 100644 --- a/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx +++ b/x-pack/plugins/data_usage/public/app/components/chart_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { useCallback, useMemo } from 'react'; -import numeral from '@elastic/numeral'; + import { EuiFlexItem, EuiPanel, EuiTitle, useEuiTheme } from '@elastic/eui'; import { Chart, @@ -20,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { LegendAction } from './legend_action'; import { MetricTypes, MetricSeries } from '../../../common/rest_types'; +import { formatBytes } from '../../utils/format_bytes'; // TODO: Remove this when we have a title for each metric type type ChartKey = Extract; @@ -118,7 +119,3 @@ export const ChartPanel: React.FC = ({ ); }; - -const formatBytes = (bytes: number) => { - return numeral(bytes).format('0.0 b'); -}; diff --git a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx index cc443c78562ee..48b6566df9e66 100644 --- a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx +++ b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useCallback, useEffect, memo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic, EuiCallOut } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic } from '@elastic/eui'; import { Charts } from './charts'; import { useBreadcrumbs } from '../../utils/use_breadcrumbs'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; @@ -16,21 +16,22 @@ import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics'; import { useDataUsageMetricsUrlParams } from '../hooks/use_charts_url_params'; import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from '../hooks/use_date_picker'; import { DEFAULT_METRIC_TYPES, UsageMetricsRequestBody } from '../../../common/rest_types'; -import { ChartFilters } from './filters/charts_filters'; -import { UX_LABELS } from '../translations'; +import { ChartFilters, ChartFiltersProps } from './filters/charts_filters'; +import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; const EuiItemCss = css` width: 100%; `; -const FlexItemWithCss = memo(({ children }: { children: React.ReactNode }) => ( +const FlexItemWithCss = ({ children }: { children: React.ReactNode }) => ( {children} -)); +); export const DataUsageMetrics = () => { const { services: { chrome, appParams }, } = useKibanaContextForPlugin(); + useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); const { metricTypes: metricTypesFromUrl, @@ -38,9 +39,17 @@ export const DataUsageMetrics = () => { startDate: startDateFromUrl, endDate: endDateFromUrl, setUrlMetricTypesFilter, + setUrlDataStreamsFilter, setUrlDateRangeFilter, } = useDataUsageMetricsUrlParams(); + const { data: dataStreams, isFetching: isFetchingDataStreams } = useGetDataUsageDataStreams({ + selectedDataStreams: dataStreamsFromUrl, + options: { + enabled: true, + }, + }); + const [metricsFilters, setMetricsFilters] = useState({ metricTypes: [...DEFAULT_METRIC_TYPES], dataStreams: [], @@ -52,15 +61,22 @@ export const DataUsageMetrics = () => { if (!metricTypesFromUrl) { setUrlMetricTypesFilter(metricsFilters.metricTypes.join(',')); } + if (!dataStreamsFromUrl && dataStreams) { + setUrlDataStreamsFilter(dataStreams.map((ds) => ds.name).join(',')); + } if (!startDateFromUrl || !endDateFromUrl) { setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to }); } }, [ + dataStreams, + dataStreamsFromUrl, endDateFromUrl, metricTypesFromUrl, + metricsFilters.dataStreams, metricsFilters.from, metricsFilters.metricTypes, metricsFilters.to, + setUrlDataStreamsFilter, setUrlDateRangeFilter, setUrlMetricTypesFilter, startDateFromUrl, @@ -77,7 +93,6 @@ export const DataUsageMetrics = () => { const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker(); const { - error, data, isFetching, isFetched, @@ -90,6 +105,7 @@ export const DataUsageMetrics = () => { }, { retry: false, + enabled: !!metricsFilters.dataStreams.length, } ); @@ -111,33 +127,51 @@ export const DataUsageMetrics = () => { [setMetricsFilters] ); - useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); + const filterOptions: ChartFiltersProps['filterOptions'] = useMemo(() => { + const dataStreamsOptions = dataStreams?.reduce>((acc, ds) => { + acc[ds.name] = ds.storageSizeBytes; + return acc; + }, {}); + + return { + dataStreams: { + filterName: 'dataStreams', + options: dataStreamsOptions ? Object.keys(dataStreamsOptions) : metricsFilters.dataStreams, + appendOptions: dataStreamsOptions, + selectedOptions: metricsFilters.dataStreams, + onChangeFilterOptions: onChangeDataStreamsFilter, + isFilterLoading: isFetchingDataStreams, + }, + metricTypes: { + filterName: 'metricTypes', + options: metricsFilters.metricTypes, + onChangeFilterOptions: onChangeMetricTypesFilter, + }, + }; + }, [ + dataStreams, + isFetchingDataStreams, + metricsFilters.dataStreams, + metricsFilters.metricTypes, + onChangeDataStreamsFilter, + onChangeMetricTypesFilter, + ]); return ( - {!isFetching && error?.message && ( - - - - )} + {isFetched && data?.metrics ? ( diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx index 466bc6debae77..83d417565f012 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx @@ -7,7 +7,7 @@ import { orderBy } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; +import { EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; import { @@ -15,7 +15,6 @@ import { type MetricTypes, } from '../../../../common/rest_types'; -import { ClearAllButton } from './clear_all_button'; import { UX_LABELS } from '../../translations'; import { ChartsFilterPopover } from './charts_filter_popover'; import { FilterItems, FilterName, useChartsFilter } from '../../hooks'; @@ -27,20 +26,34 @@ const getSearchPlaceholder = (filterName: FilterName) => { return UX_LABELS.filterSearchPlaceholder('metric types'); }; -export const ChartsFilter = memo( +export interface ChartsFilterProps { + filterOptions: { + filterName: FilterName; + options: string[]; + appendOptions?: Record; + selectedOptions?: string[]; + onChangeFilterOptions: (selectedOptions: string[]) => void; + isFilterLoading?: boolean; + }; + 'data-test-subj'?: string; +} + +export const ChartsFilter = memo( ({ - filterName, - onChangeFilterOptions, + filterOptions: { + filterName, + options, + appendOptions, + selectedOptions, + onChangeFilterOptions, + isFilterLoading = false, + }, 'data-test-subj': dataTestSubj, - }: { - filterName: FilterName; - onChangeFilterOptions?: (selectedOptions: string[]) => void; - 'data-test-subj'?: string; }) => { const getTestId = useTestIdGenerator(dataTestSubj); - const isMetricsFilter = filterName === 'metricTypes'; const isDataStreamsFilter = filterName === 'dataStreams'; + // popover states and handlers const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onPopoverButtonClick = useCallback(() => { @@ -50,11 +63,8 @@ export const ChartsFilter = memo( setIsPopoverOpen(false); }, [setIsPopoverOpen]); - // search string state - const [searchString, setSearchString] = useState(''); const { areDataStreamsSelectedOnMount, - isLoading, items, setItems, hasActiveFilters, @@ -64,17 +74,18 @@ export const ChartsFilter = memo( setUrlDataStreamsFilter, setUrlMetricTypesFilter, } = useChartsFilter({ - filterName, - searchString, + filterOptions: { + filterName, + options, + appendOptions, + selectedOptions, + onChangeFilterOptions, + isFilterLoading, + }, }); // track popover state to pin selected options const wasPopoverOpen = useRef(isPopoverOpen); - useEffect(() => { - return () => { - wasPopoverOpen.current = isPopoverOpen; - }; - }, [isPopoverOpen, wasPopoverOpen]); // compute if selected dataStreams should be pinned const shouldPinSelectedDataStreams = useCallback( @@ -104,8 +115,16 @@ export const ChartsFilter = memo( const onOptionsChange = useCallback( (newOptions: FilterItems) => { + const optionItemsToSet = newOptions.map((option) => option); + const currChecks = optionItemsToSet.filter((option) => option.checked === 'on'); + + // don't update filter state if trying to uncheck all options + if (currChecks.length < 1) { + return; + } + // update filter UI options state - setItems(newOptions.map((option) => option)); + setItems(optionItemsToSet); // compute a selected list of options const selectedItems = newOptions.reduce((acc, curr) => { @@ -129,10 +148,7 @@ export const ChartsFilter = memo( shouldPinSelectedDataStreams(false); setAreDataStreamsSelectedOnMount(false); - // update overall query state - if (typeof onChangeFilterOptions !== 'undefined') { - onChangeFilterOptions(selectedItems); - } + onChangeFilterOptions(selectedItems); }, [ setItems, @@ -146,35 +162,11 @@ export const ChartsFilter = memo( ] ); - // clear all selected options - const onClearAll = useCallback(() => { - // update filter UI options state - setItems( - items.map((option) => { - option.checked = undefined; - return option; - }) - ); - - // update URL params based on filter on page - if (isMetricsFilter) { - setUrlMetricTypesFilter(''); - } else if (isDataStreamsFilter) { - setUrlDataStreamsFilter(''); - } - - if (typeof onChangeFilterOptions !== 'undefined') { - onChangeFilterOptions([]); - } - }, [ - setItems, - items, - isMetricsFilter, - isDataStreamsFilter, - onChangeFilterOptions, - setUrlMetricTypesFilter, - setUrlDataStreamsFilter, - ]); + useEffect(() => { + return () => { + wasPopoverOpen.current = isPopoverOpen; + }; + }, [isPopoverOpen, wasPopoverOpen]); return ( setSearchString(searchValue.trim()), }} > {(list, search) => { @@ -215,17 +206,6 @@ export const ChartsFilter = memo( )} {list} - {!isMetricsFilter && ( - - - - - - )} ); }} diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx index 72608f4a62c75..6f3b07e37dc83 100644 --- a/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx +++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx @@ -14,13 +14,13 @@ import type { import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; import { useGetDataUsageMetrics } from '../../../hooks/use_get_usage_metrics'; import { DateRangePickerValues, UsageMetricsDateRangePicker } from './date_picker'; -import { ChartsFilter } from './charts_filter'; +import { ChartsFilter, ChartsFilterProps } from './charts_filter'; +import { FilterName } from '../../hooks'; -interface ChartFiltersProps { +export interface ChartFiltersProps { dateRangePickerState: DateRangePickerValues; isDataLoading: boolean; - onChangeDataStreamsFilter: (selectedDataStreams: string[]) => void; - onChangeMetricTypesFilter?: (selectedMetricTypes: string[]) => void; + filterOptions: Record; onRefresh: () => void; onRefreshChange: (evt: OnRefreshChangeProps) => void; onTimeChange: ({ start, end }: DurationRange) => void; @@ -33,9 +33,8 @@ export const ChartFilters = memo( ({ dateRangePickerState, isDataLoading, + filterOptions, onClick, - onChangeMetricTypesFilter, - onChangeDataStreamsFilter, onRefresh, onRefreshChange, onTimeChange, @@ -47,19 +46,13 @@ export const ChartFilters = memo( const filters = useMemo(() => { return ( <> - {showMetricsTypesFilter && ( - + {showMetricsTypesFilter && } + {!filterOptions.dataStreams.isFilterLoading && ( + )} - ); - }, [onChangeDataStreamsFilter, onChangeMetricTypesFilter, showMetricsTypesFilter]); + }, [filterOptions, showMetricsTypesFilter]); const onClickRefreshButton = useCallback(() => onClick(), [onClick]); diff --git a/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx b/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx deleted file mode 100644 index afa4c2fe72917..0000000000000 --- a/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx +++ /dev/null @@ -1,43 +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, { memo } from 'react'; -import { css } from '@emotion/react'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { UX_LABELS } from '../../translations'; - -const buttonCss = css` - border-top: ${euiThemeVars.euiBorderThin}; - border-radius: 0; -`; -export const ClearAllButton = memo( - ({ - 'data-test-subj': dataTestSubj, - isDisabled, - onClick, - }: { - 'data-test-subj'?: string; - isDisabled: boolean; - onClick: () => void; - }) => { - return ( - - {UX_LABELS.filterClearAll} - - ); - } -); - -ClearAllButton.displayName = 'ClearAllButton'; diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx index 330c9a633396d..5cff100d9752e 100644 --- a/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx +++ b/x-pack/plugins/data_usage/public/app/hooks/use_charts_filter.tsx @@ -11,9 +11,10 @@ import { METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP, METRIC_TYPE_VALUES, } from '../../../common/rest_types'; -import { useGetDataUsageDataStreams } from '../../hooks/use_get_data_streams'; import { FILTER_NAMES } from '../translations'; import { useDataUsageMetricsUrlParams } from './use_charts_url_params'; +import { formatBytes } from '../../utils/format_bytes'; +import { ChartsFilterProps } from '../components/filters/charts_filter'; export type FilterName = keyof typeof FILTER_NAMES; @@ -26,14 +27,11 @@ export type FilterItems = Array<{ }>; export const useChartsFilter = ({ - filterName, - searchString, + filterOptions, }: { - filterName: FilterName; - searchString: string; + filterOptions: ChartsFilterProps['filterOptions']; }): { areDataStreamsSelectedOnMount: boolean; - isLoading: boolean; items: FilterItems; setItems: React.Dispatch>; hasActiveFilters: boolean; @@ -52,12 +50,8 @@ export const useChartsFilter = ({ setUrlMetricTypesFilter, setUrlDataStreamsFilter, } = useDataUsageMetricsUrlParams(); - const isMetricTypesFilter = filterName === 'metricTypes'; - const isDataStreamsFilter = filterName === 'dataStreams'; - const { data: dataStreams, isFetching } = useGetDataUsageDataStreams({ - searchString, - selectedDataStreams: selectedDataStreamsFromUrl, - }); + const isMetricTypesFilter = filterOptions.filterName === 'metricTypes'; + const isDataStreamsFilter = filterOptions.filterName === 'dataStreams'; // track the state of selected data streams via URL // when the page is loaded via selected data streams on URL @@ -80,24 +74,23 @@ export const useChartsFilter = ({ label: METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP[metricType], checked: isDefaultMetricType(metricType) ? 'on' : undefined, // default metrics are selected by default disabled: isDefaultMetricType(metricType), - 'data-test-subj': `${filterName}-filter-option`, + 'data-test-subj': `${filterOptions.filterName}-filter-option`, + })) + : isDataStreamsFilter && !!filterOptions.options.length + ? filterOptions.options?.map((filterOption) => ({ + key: filterOption, + label: filterOption, + append: formatBytes(filterOptions.appendOptions?.[filterOption] ?? 0), + checked: selectedDataStreamsFromUrl + ? selectedDataStreamsFromUrl.includes(filterOption) + ? 'on' + : undefined + : 'on', + 'data-test-subj': `${filterOptions.filterName}-filter-option`, })) : [] ); - useEffect(() => { - if (isDataStreamsFilter && dataStreams) { - setItems( - dataStreams?.map((dataStream) => ({ - key: dataStream.name, - label: dataStream.name, - checked: dataStream.selected ? 'on' : undefined, - 'data-test-subj': `${filterName}-filter-option`, - })) - ); - } - }, [dataStreams, filterName, isDataStreamsFilter, setItems]); - const hasActiveFilters = useMemo(() => !!items.find((item) => item.checked === 'on'), [items]); const numActiveFilters = useMemo( () => items.filter((item) => item.checked === 'on').length, @@ -110,7 +103,6 @@ export const useChartsFilter = ({ return { areDataStreamsSelectedOnMount, - isLoading: isDataStreamsFilter && isFetching, items, setItems, hasActiveFilters, diff --git a/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx index 0e03da5d9adbd..ed833393ad7eb 100644 --- a/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx +++ b/x-pack/plugins/data_usage/public/app/hooks/use_charts_url_params.tsx @@ -53,9 +53,7 @@ export const getDataUsageMetricsFiltersFromUrlParams = ( }, []) : []; - const urlDataStreams = urlParams.dataStreams - ? String(urlParams.dataStreams).split(',').sort() - : []; + const urlDataStreams = urlParams.dataStreams ? String(urlParams.dataStreams).split(',') : []; dataUsageMetricsFilters.metricTypes = urlMetricTypes.length ? urlMetricTypes : undefined; dataUsageMetricsFilters.dataStreams = urlDataStreams.length ? urlDataStreams : undefined; diff --git a/x-pack/plugins/data_usage/public/app/translations.tsx b/x-pack/plugins/data_usage/public/app/translations.tsx index 687cdcf499b0d..ee42d3b58906b 100644 --- a/x-pack/plugins/data_usage/public/app/translations.tsx +++ b/x-pack/plugins/data_usage/public/app/translations.tsx @@ -48,7 +48,4 @@ export const UX_LABELS = Object.freeze({ defaultMessage: 'No {filterName} available', values: { filterName }, }), - noDataStreamsSelected: i18n.translate('xpack.dataUsage.metrics.noDataStreamsSelected', { - defaultMessage: 'Select one or more data streams to view data usage metrics.', - }), }); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts index 59b36e156a824..46a448ac82b31 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_data_streams.ts @@ -13,6 +13,7 @@ import { useKibanaContextForPlugin } from '../utils/use_kibana'; type GetDataUsageDataStreamsResponse = Array<{ name: string; + storageSizeBytes: number; selected: boolean; }>; @@ -22,11 +23,11 @@ const PAGING_PARAMS = Object.freeze({ }); export const useGetDataUsageDataStreams = ({ - searchString, selectedDataStreams, - options = {}, + options = { + enabled: false, + }, }: { - searchString: string; selectedDataStreams?: string[]; options?: UseQueryOptions; }): UseQueryResult => { @@ -41,7 +42,7 @@ export const useGetDataUsageDataStreams = ({ DATA_USAGE_DATA_STREAMS_API_ROUTE, { version: '1', - query: {}, + // query: {}, } ); @@ -49,12 +50,14 @@ export const useGetDataUsageDataStreams = ({ selected: GetDataUsageDataStreamsResponse; rest: GetDataUsageDataStreamsResponse; }>( - (acc, list) => { + (acc, ds) => { const item = { - name: list.name, + name: ds.name, + storageSizeBytes: ds.storageSizeBytes, + selected: ds.selected, }; - if (selectedDataStreams?.includes(list.name)) { + if (selectedDataStreams?.includes(ds.name)) { acc.selected.push({ ...item, selected: true }); } else { acc.rest.push({ ...item, selected: false }); diff --git a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts index 3998c736c839e..4e89a7a3f5f0e 100644 --- a/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts +++ b/x-pack/plugins/data_usage/public/hooks/use_get_usage_metrics.ts @@ -27,8 +27,9 @@ export const useGetDataUsageMetrics = ( queryKey: ['get-data-usage-metrics', body], ...options, keepPreviousData: true, - queryFn: async () => { + queryFn: async ({ signal }) => { return http.post(DATA_USAGE_METRICS_API_ROUTE, { + signal, version: '1', body: JSON.stringify({ from: body.from, diff --git a/x-pack/plugins/data_usage/public/utils/format_bytes.ts b/x-pack/plugins/data_usage/public/utils/format_bytes.ts new file mode 100644 index 0000000000000..c5f98f3f9e0d9 --- /dev/null +++ b/x-pack/plugins/data_usage/public/utils/format_bytes.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import numeral from '@elastic/numeral'; + +export const formatBytes = (bytes: number) => { + return numeral(bytes).format('0.0 b'); +}; diff --git a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts index 5794d06f16ead..d061aa14417df 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/data_streams_handler.ts @@ -44,7 +44,7 @@ export const getDataStreamsHandler = ( .sort((a, b) => b.size_in_bytes - a.size_in_bytes) .map((stat) => ({ name: stat.name, - storageSizeBytes: stat.size_in_bytes, + storageSizeBytes: stat.size_in_bytes ?? 0, })); return response.ok({ diff --git a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts index 2b68dc3d37a64..6c188662e2237 100644 --- a/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts +++ b/x-pack/plugins/data_usage/server/routes/internal/usage_metrics_handler.ts @@ -35,6 +35,8 @@ export const getUsageMetricsHandler = ( logger.debug(`Retrieving usage metrics`); const { from, to, metricTypes, dataStreams: requestDsNames } = request.body; + // redundant check as we don't allow making requests via UI without data streams, + // but it's here to make sure the request body is validated before requesting metrics from auto-ops if (!requestDsNames?.length) { return errorHandler( logger, diff --git a/x-pack/plugins/data_usage/tsconfig.json b/x-pack/plugins/data_usage/tsconfig.json index 6d3818b88b9fe..78c501922f239 100644 --- a/x-pack/plugins/data_usage/tsconfig.json +++ b/x-pack/plugins/data_usage/tsconfig.json @@ -28,7 +28,6 @@ "@kbn/core-chrome-browser", "@kbn/features-plugin", "@kbn/index-management-shared-types", - "@kbn/ui-theme", "@kbn/repo-info", "@kbn/cloud-plugin", "@kbn/server-http-tools", diff --git a/x-pack/plugins/data_visualizer/public/application/data_drift/document_count_chart_single_brush/single_brush.tsx b/x-pack/plugins/data_visualizer/public/application/data_drift/document_count_chart_single_brush/single_brush.tsx index b6b3a32f628f1..4e0af369a8124 100644 --- a/x-pack/plugins/data_visualizer/public/application/data_drift/document_count_chart_single_brush/single_brush.tsx +++ b/x-pack/plugins/data_visualizer/public/application/data_drift/document_count_chart_single_brush/single_brush.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { isEqual } from 'lodash'; import React, { useEffect, useRef, type FC } from 'react'; import * as d3Brush from 'd3-brush'; @@ -310,9 +311,42 @@ export const SingleBrush: FC = (props) => { mlBrushSelection.exit().remove(); } + function updateBrush() { + const mlBrushSelection = gBrushes + .selectAll('.brush') + .data(brushes.current, (d) => (d as SingleBrush).id); + + mlBrushSelection.each(function (brushObject, i, n) { + const x = d3 + .scaleLinear() + .domain([minRef.current, maxRef.current]) + .rangeRound([0, widthRef.current]); + brushObject.brush.extent([ + [0, BRUSH_MARGIN], + [widthRef.current, BRUSH_HEIGHT - BRUSH_MARGIN], + ]); + + brushObject.brush(d3.select(n[i] as SVGGElement)); + const xStart = x(brushObject.start) ?? 0; + const xEnd = x(brushObject.end) ?? 0; + brushObject.brush.move(d3.select(n[i] as SVGGElement), [xStart, xEnd]); + }); + } + if (brushes.current.length !== 1) { widthRef.current = width; newBrush(`${brushId}`, baselineMin, baselineMax); + } else if ( + widthRef.current !== width || + minRef.current !== min || + maxRef.current !== max || + !isEqual(snapTimestampsRef.current, snapTimestamps) + ) { + widthRef.current = width; + minRef.current = min; + maxRef.current = max; + snapTimestampsRef.current = snapTimestamps; + updateBrush(); } drawBrushes(); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts.ts new file mode 100644 index 0000000000000..e6fb24e3831c7 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Document } from '@langchain/core/documents'; + +export const mockAnonymizedAlerts: Document[] = [ + { + pageContent: + '@timestamp,2024-10-16T02:40:08.837Z\n_id,87c42d26897490ee02ba42ec4e872910b29f3c69bda357b8faf197b533b8528a\nevent.category,malware,intrusion_detection\nevent.dataset,endpoint.alerts\nevent.module,endpoint\nevent.outcome,success\nhost.name,f5b69281-3e7e-4b52-9225-e5c30dc29c78\nhost.os.name,Windows\nhost.os.version,21H2 (10.0.20348.1607)\nkibana.alert.original_time,2023-04-01T22:03:26.909Z\nkibana.alert.risk_score,99\nkibana.alert.rule.description,Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.\nkibana.alert.rule.name,Malicious Behavior Detection Alert: Execution of a Windows Script File Written by a Suspicious Process\nkibana.alert.severity,critical\nkibana.alert.workflow_status,open\nmessage,Malicious Behavior Detection Alert: Execution of a Windows Script File Written by a Suspicious Process\nprocess.Ext.token.integrity_level_name,high\nprocess.args,wscript,C:\\ProgramData\\WindowsAppPool\\AppPool.vbs\nprocess.code_signature.exists,true\nprocess.code_signature.status,trusted\nprocess.code_signature.subject_name,Microsoft Windows\nprocess.code_signature.trusted,true\nprocess.command_line,wscript C:\\ProgramData\\WindowsAppPool\\AppPool.vbs\nprocess.executable,C:\\Windows\\System32\\wscript.exe\nprocess.hash.md5,3412340ca1bf2f4118cbfe98961ceeda\nprocess.hash.sha1,bcb0568cbf0af0c09b53829ce9ee8ba30db77c56\nprocess.hash.sha256,02c731754bcc8f063a8c7aa53c7b7d5773f389e17582ffaa6eaaa692da183fd7\nprocess.name,wscript.exe\nprocess.parent.args,C:\\Program Files\\Microsoft Office\\Root\\Office16\\WINWORD.EXE,/n,C:\\Users\\Administrator\\Desktop\\9828375091\\7cbad6b3f505a199d6766a86b41ed23786bbb99dab9cae6c18936afdc2512f00.doc,/o,\nprocess.parent.args_count,5\nprocess.parent.command_line,"C:\\Program Files\\Microsoft Office\\Root\\Office16\\WINWORD.EXE" /n "C:\\Users\\Administrator\\Desktop\\9828375091\\7cbad6b3f505a199d6766a86b41ed23786bbb99dab9cae6c18936afdc2512f00.doc" /o ""\nprocess.parent.executable,C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE\nprocess.parent.name,WINWORD.EXE\nprocess.pe.original_file_name,wscript.exe\nprocess.pid,13024\nprocess.working_directory,C:\\Users\\Administrator\\Desktop\\9828375091\\\nrule.name,Execution of a Windows Script File Written by a Suspicious Process\nthreat.framework,MITRE ATT&CK,MITRE ATT&CK\nthreat.tactic.id,TA0002,TA0005\nthreat.tactic.name,Execution,Defense Evasion\nthreat.tactic.reference,https://attack.mitre.org/tactics/TA0002/,https://attack.mitre.org/tactics/TA0005/\nthreat.technique.id,T1059,T1218\nthreat.technique.name,Command and Scripting Interpreter,System Binary Proxy Execution\nthreat.technique.reference,https://attack.mitre.org/techniques/T1059/,https://attack.mitre.org/techniques/T1218/\nthreat.technique.subtechnique.id,T1059.005,T1059.007,T1059.001,T1218.005\nthreat.technique.subtechnique.name,Visual Basic,JavaScript,PowerShell,Mshta\nthreat.technique.subtechnique.reference,https://attack.mitre.org/techniques/T1059/005/,https://attack.mitre.org/techniques/T1059/007/,https://attack.mitre.org/techniques/T1059/001/,https://attack.mitre.org/techniques/T1218/005/\nuser.domain,OMM-WIN-DETECT\nuser.name,42c4e419-c859-47a5-b1cb-f069d48fa509', + metadata: {}, + }, + { + pageContent: + '@timestamp,2024-10-16T02:40:08.836Z\n_id,be6d293f9a71ba209adbcacc3ba04adfd8e9456260f6af342b7cb0478a7a144a\nevent.category,malware,intrusion_detection\nevent.dataset,endpoint.alerts\nevent.module,endpoint\nevent.outcome,success\nfile.name,AppPool.vbs\nfile.path,C:\\ProgramData\\WindowsAppPool\\AppPool.vbs\nhost.name,f5b69281-3e7e-4b52-9225-e5c30dc29c78\nhost.os.name,Windows\nhost.os.version,21H2 (10.0.20348.1607)\nkibana.alert.original_time,2023-04-01T22:03:26.747Z\nkibana.alert.risk_score,99\nkibana.alert.rule.description,Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.\nkibana.alert.rule.name,Malicious Behavior Detection Alert: Suspicious Executable File Creation\nkibana.alert.severity,critical\nkibana.alert.workflow_status,open\nmessage,Malicious Behavior Detection Alert: Suspicious Executable File Creation\nprocess.code_signature.exists,true\nprocess.code_signature.status,trusted\nprocess.code_signature.subject_name,Microsoft Corporation\nprocess.code_signature.trusted,true\nprocess.executable,C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE\nprocess.name,WINWORD.EXE\nprocess.pid,13036\nrule.name,Suspicious Executable File Creation\nthreat.framework,MITRE ATT&CK,MITRE ATT&CK\nthreat.tactic.id,TA0011,TA0002\nthreat.tactic.name,Command and Control,Execution\nthreat.tactic.reference,https://attack.mitre.org/tactics/TA0011/,https://attack.mitre.org/tactics/TA0002/\nthreat.technique.id,T1105,T1059\nthreat.technique.name,Ingress Tool Transfer,Command and Scripting Interpreter\nthreat.technique.reference,https://attack.mitre.org/techniques/T1105/,https://attack.mitre.org/techniques/T1059/\nthreat.technique.subtechnique.id,T1059.005,T1059.007\nthreat.technique.subtechnique.name,Visual Basic,JavaScript\nthreat.technique.subtechnique.reference,https://attack.mitre.org/techniques/T1059/005/,https://attack.mitre.org/techniques/T1059/007/\nuser.domain,OMM-WIN-DETECT\nuser.name,42c4e419-c859-47a5-b1cb-f069d48fa509', + metadata: {}, + }, +]; + +export const mockAnonymizedAlertsReplacements: Record = { + '42c4e419-c859-47a5-b1cb-f069d48fa509': 'Administrator', + 'f5b69281-3e7e-4b52-9225-e5c30dc29c78': 'SRVWIN07', +}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries.ts new file mode 100644 index 0000000000000..d8d66481571d7 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AttackDiscovery } from '@kbn/elastic-assistant-common'; + +export const mockAttackDiscoveries: AttackDiscovery[] = [ + { + title: 'Critical Malware and Phishing Alerts on host e1cb3cf0-30f3-4f99-a9c8-518b955c6f90', + alertIds: [ + '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', + 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', + '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', + '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', + 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', + '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', + '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', + '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', + ], + timestamp: '2024-10-10T22:59:52.749Z', + detailsMarkdown: + '- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} running {{ host.os.name macOS }} version {{ host.os.version 13.4 }}.\n- The malware was identified as {{ file.name unix1 }} with SHA256 hash {{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.\n- Another critical alert was triggered for potential credentials phishing via {{ process.name osascript }} on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process {{ process.name osascript }} was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.', + summaryMarkdown: + 'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}. Malware identified as {{ file.name unix1 }} and phishing attempt via {{ process.name osascript }}.', + mitreAttackTactics: ['Credential Access', 'Input Capture'], + entitySummaryMarkdown: + 'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.', + }, +]; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_experiment_connector.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_experiment_connector.ts new file mode 100644 index 0000000000000..1ee32768ab091 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_experiment_connector.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; + +export const mockExperimentConnector: Connector = { + name: 'Gemini 1.5 Pro 002', + actionTypeId: '.gemini', + config: { + apiUrl: 'https://example.com', + defaultModel: 'gemini-1.5-pro-002', + gcpRegion: 'test-region', + gcpProjectID: 'test-project-id', + }, + secrets: { + credentialsJson: '{}', + }, + id: 'gemini-1-5-pro-002', + isPreconfigured: true, + isSystemAction: false, + isDeprecated: false, +} as Connector; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/index.test.ts new file mode 100644 index 0000000000000..8154f0b446566 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/index.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { ActionsClient } from '@kbn/actions-plugin/server'; +import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import type { ActionsClientLlm } from '@kbn/langchain/server'; +import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; +import { loggerMock } from '@kbn/logging-mocks'; +import type { LangChainTracer } from '@langchain/core/tracers/tracer_langchain'; + +import { evaluateAttackDiscovery } from '.'; +import { DefaultAttackDiscoveryGraph } from '../graphs/default_attack_discovery_graph'; +import { AttackDiscoveryGraphMetadata } from '../../langchain/graphs'; +import { mockExperimentConnector } from './__mocks__/mock_experiment_connector'; +import { getLlmType } from '../../../routes/utils'; + +jest.mock('@kbn/langchain/server', () => ({ + ...jest.requireActual('@kbn/langchain/server'), + + ActionsClientLlm: jest.fn(), +})); + +jest.mock('langsmith/evaluation', () => ({ + evaluate: jest.fn(async (predict: Function) => + predict({ + overrides: { + errors: ['test-error'], + }, + }) + ), +})); + +jest.mock('./helpers/get_custom_evaluator', () => ({ + getCustomEvaluator: jest.fn(), +})); + +jest.mock('./helpers/get_evaluator_llm', () => { + const mockLlm = jest.fn() as unknown as ActionsClientLlm; + + return { + getEvaluatorLlm: jest.fn().mockResolvedValue(mockLlm), + }; +}); + +const actionsClient = { + get: jest.fn(), +} as unknown as ActionsClient; +const alertsIndexPattern = 'test-alerts-index-pattern'; +const connectorTimeout = 1000; +const datasetName = 'test-dataset'; +const evaluationId = 'test-evaluation-id'; +const evaluatorConnectorId = 'test-evaluator-connector-id'; +const langSmithApiKey = 'test-api-key'; +const langSmithProject = 'test-lang-smith-project'; +const logger = loggerMock.create(); +const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); +const runName = 'test-run-name'; + +const connectors = [mockExperimentConnector]; + +const projectName = 'test-lang-smith-project'; + +const graphs: Array<{ + connector: Connector; + graph: DefaultAttackDiscoveryGraph; + llmType: string | undefined; + name: string; + traceOptions: { + projectName: string | undefined; + tracers: LangChainTracer[]; + }; +}> = connectors.map((connector) => { + const llmType = getLlmType(connector.actionTypeId); + + const traceOptions = { + projectName, + tracers: [ + ...getLangSmithTracer({ + apiKey: langSmithApiKey, + projectName, + logger, + }), + ], + }; + + const graph = { + invoke: jest.fn().mockResolvedValue({}), + } as unknown as DefaultAttackDiscoveryGraph; + + return { + connector, + graph, + llmType, + name: `${runName} - ${connector.name} - ${evaluationId} - Attack discovery`, + traceOptions, + }; +}); + +const attackDiscoveryGraphs: AttackDiscoveryGraphMetadata[] = [ + { + getDefaultAttackDiscoveryGraph: jest.fn().mockReturnValue(graphs[0].graph), + graphType: 'attack-discovery', + }, +]; + +describe('evaluateAttackDiscovery', () => { + beforeEach(() => jest.clearAllMocks()); + + it('evaluates the attack discovery graphs', async () => { + await evaluateAttackDiscovery({ + actionsClient, + attackDiscoveryGraphs, + alertsIndexPattern, + connectors, + connectorTimeout, + datasetName, + esClient: mockEsClient, + evaluationId, + evaluatorConnectorId, + langSmithApiKey, + langSmithProject, + logger, + runName, + size: 20, + }); + + expect(graphs[0].graph.invoke).toHaveBeenCalledWith( + { + errors: ['test-error'], + }, + { + callbacks: [...graphs[0].traceOptions.tracers], + runName: graphs[0].name, + tags: ['evaluation', graphs[0].llmType ?? ''], + } + ); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/run_evaluations/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/run_evaluations/index.test.ts new file mode 100644 index 0000000000000..909c279218f1c --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/run_evaluations/index.test.ts @@ -0,0 +1,141 @@ +/* + * 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 { ActionsClient } from '@kbn/actions-plugin/server'; +import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; +import type { ActionsClientLlm } from '@kbn/langchain/server'; +import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; +import { loggerMock } from '@kbn/logging-mocks'; +import type { LangChainTracer } from '@langchain/core/tracers/tracer_langchain'; + +import { runEvaluations } from '.'; +import { type DefaultAttackDiscoveryGraph } from '../../graphs/default_attack_discovery_graph'; +import { mockExperimentConnector } from '../__mocks__/mock_experiment_connector'; +import { getLlmType } from '../../../../routes/utils'; + +jest.mock('@kbn/langchain/server', () => ({ + ...jest.requireActual('@kbn/langchain/server'), + + ActionsClientLlm: jest.fn(), +})); + +jest.mock('langsmith/evaluation', () => ({ + evaluate: jest.fn(async (predict: Function) => + predict({ + overrides: { + errors: ['test-error'], + }, + }) + ), +})); + +jest.mock('../helpers/get_custom_evaluator', () => ({ + getCustomEvaluator: jest.fn(), +})); + +jest.mock('../helpers/get_evaluator_llm', () => { + const mockLlm = jest.fn() as unknown as ActionsClientLlm; + + return { + getEvaluatorLlm: jest.fn().mockResolvedValue(mockLlm), + }; +}); + +const actionsClient = { + get: jest.fn(), +} as unknown as ActionsClient; +const connectorTimeout = 1000; +const datasetName = 'test-dataset'; +const evaluatorConnectorId = 'test-evaluator-connector-id'; +const langSmithApiKey = 'test-api-key'; +const logger = loggerMock.create(); +const connectors = [mockExperimentConnector]; + +const projectName = 'test-lang-smith-project'; + +const graphs: Array<{ + connector: Connector; + graph: DefaultAttackDiscoveryGraph; + llmType: string | undefined; + name: string; + traceOptions: { + projectName: string | undefined; + tracers: LangChainTracer[]; + }; +}> = connectors.map((connector) => { + const llmType = getLlmType(connector.actionTypeId); + + const traceOptions = { + projectName, + tracers: [ + ...getLangSmithTracer({ + apiKey: langSmithApiKey, + projectName, + logger, + }), + ], + }; + + const graph = { + invoke: jest.fn().mockResolvedValue({}), + } as unknown as DefaultAttackDiscoveryGraph; + + return { + connector, + graph, + llmType, + name: `testRunName - ${connector.name} - testEvaluationId - Attack discovery`, + traceOptions, + }; +}); + +describe('runEvaluations', () => { + beforeEach(() => jest.clearAllMocks()); + + it('predict() invokes the graph with the expected overrides', async () => { + await runEvaluations({ + actionsClient, + connectorTimeout, + datasetName, + evaluatorConnectorId, + graphs, + langSmithApiKey, + logger, + }); + + expect(graphs[0].graph.invoke).toHaveBeenCalledWith( + { + errors: ['test-error'], + }, + { + callbacks: [...graphs[0].traceOptions.tracers], + runName: graphs[0].name, + tags: ['evaluation', graphs[0].llmType ?? ''], + } + ); + }); + + it('catches and logs errors that occur during evaluation', async () => { + const error = new Error('Test error'); + + (graphs[0].graph.invoke as jest.Mock).mockRejectedValue(error); + + await runEvaluations({ + actionsClient, + connectorTimeout, + datasetName, + evaluatorConnectorId, + graphs, + langSmithApiKey, + logger, + }); + + expect(logger.error).toHaveBeenCalledWith( + 'Error evaluating connector "Gemini 1.5 Pro 002" (gemini), running experiment "testRunName - Gemini 1.5 Pro 002 - testEvaluationId - Attack discovery": Error: Test error' + ); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_results/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_results/index.test.ts new file mode 100644 index 0000000000000..b589fab8e5797 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_results/index.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AttackDiscovery } from '@kbn/elastic-assistant-common'; + +import { getHasResults } from '.'; + +const attackDiscoveries: AttackDiscovery[] = [ + { + title: 'Critical Malware and Phishing Alerts on host e1cb3cf0-30f3-4f99-a9c8-518b955c6f90', + alertIds: [ + '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', + 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', + '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', + '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', + 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', + '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', + '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', + '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', + ], + timestamp: '2024-10-10T22:59:52.749Z', + detailsMarkdown: + '- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} running {{ host.os.name macOS }} version {{ host.os.version 13.4 }}.\n- The malware was identified as {{ file.name unix1 }} with SHA256 hash {{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.\n- Another critical alert was triggered for potential credentials phishing via {{ process.name osascript }} on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process {{ process.name osascript }} was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.', + summaryMarkdown: + 'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}. Malware identified as {{ file.name unix1 }} and phishing attempt via {{ process.name osascript }}.', + mitreAttackTactics: ['Credential Access', 'Input Capture'], + entitySummaryMarkdown: + 'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.', + }, +]; + +describe('getHasResults', () => { + it('returns true when attackDiscoveries is not null', () => { + expect(getHasResults(attackDiscoveries)).toBe(true); + }); + + it('returns false when attackDiscoveries is null', () => { + expect(getHasResults(null)).toBe(false); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_zero_alerts/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_zero_alerts/index.test.ts new file mode 100644 index 0000000000000..2c500c375db0b --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_zero_alerts/index.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getHasZeroAlerts } from '.'; +import { mockAnonymizedAlerts } from '../../../../../evaluation/__mocks__/mock_anonymized_alerts'; + +describe('getHasZeroAlerts', () => { + it('returns true when there are no alerts', () => { + expect(getHasZeroAlerts([])).toBe(true); + }); + + it('returns false when there are alerts', () => { + expect(getHasZeroAlerts(mockAnonymizedAlerts)).toBe(false); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_refine_or_end_decision/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_refine_or_end_decision/index.test.ts new file mode 100644 index 0000000000000..87f73402a3a2d --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_refine_or_end_decision/index.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRefineOrEndDecision } from '.'; + +describe('getRefineOrEndDecision', () => { + it("returns 'end' when the refined results were generated", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: true, + maxHallucinationFailuresReached: false, + maxRetriesReached: false, + }); + + expect(result).toEqual('end'); + }); + + describe('limits shared by both the generate and refine steps', () => { + it("returns 'end' when the (shared) max hallucinations limit was reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: true, + maxRetriesReached: false, + }); + + expect(result).toEqual('end'); + }); + + it("returns 'end' when the (shared) max generation attempts limit was reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: false, + maxRetriesReached: true, + }); + + expect(result).toEqual('end'); + }); + + it("returns 'end' when multiple limits are reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: true, // <-- limit reached + maxRetriesReached: true, // <-- another limit reached + }); + + expect(result).toEqual('end'); + }); + }); + + it("returns 'refine' when there are unrefined results, and limits have NOT been reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: false, + maxRetriesReached: false, + }); + + expect(result).toEqual('refine'); + }); + + describe('getRefineOrEndDecision', () => { + it("returns 'end' when the refined results were generated", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: true, + maxHallucinationFailuresReached: false, + maxRetriesReached: false, + }); + + expect(result).toEqual('end'); + }); + + describe('limits shared by both the generate and refine steps', () => { + it("returns 'end' when the (shared) max hallucinations limit was reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: true, + maxRetriesReached: false, + }); + + expect(result).toEqual('end'); + }); + + it("returns 'end' when the (shared) max generation attempts limit was reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: false, + maxRetriesReached: true, + }); + + expect(result).toEqual('end'); + }); + + it("returns 'end' when multiple limits are reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: true, // <-- limit reached + maxRetriesReached: true, // <-- another limit reached + }); + + expect(result).toEqual('end'); + }); + }); + + it("returns 'refine' when there are unrefined results, and limits have NOT been reached", () => { + const result = getRefineOrEndDecision({ + hasFinalResults: false, + maxHallucinationFailuresReached: false, + maxRetriesReached: false, + }); + + expect(result).toEqual('refine'); + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_should_end/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_should_end/index.test.ts new file mode 100644 index 0000000000000..8c35773f8bea2 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_should_end/index.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { getShouldEnd } from '.'; + +describe('getShouldEnd', () => { + it('returns true when hasFinalResults is true', () => { + const result = getShouldEnd({ + hasFinalResults: true, + maxHallucinationFailuresReached: false, + maxRetriesReached: false, + }); + + expect(result).toBe(true); + }); + + it('returns true when maxHallucinationFailuresReached is true', () => { + const result = getShouldEnd({ + hasFinalResults: false, + maxHallucinationFailuresReached: true, + maxRetriesReached: false, + }); + + expect(result).toBe(true); + }); + + it('returns true when maxRetriesReached is true', () => { + const result = getShouldEnd({ + hasFinalResults: false, + maxHallucinationFailuresReached: false, + maxRetriesReached: true, + }); + + expect(result).toBe(true); + }); + + it('returns true when both maxHallucinationFailuresReached and maxRetriesReached are true', () => { + const result = getShouldEnd({ + hasFinalResults: false, + maxHallucinationFailuresReached: true, // <-- limit reached + maxRetriesReached: true, // <-- another limit reached + }); + + expect(result).toBe(true); + }); + + it('returns false when all conditions are false', () => { + const result = getShouldEnd({ + hasFinalResults: false, + maxHallucinationFailuresReached: false, + maxRetriesReached: false, + }); + + expect(result).toBe(false); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/index.test.ts new file mode 100644 index 0000000000000..39934255c069c --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/index.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock } from '@kbn/logging-mocks'; + +import { getRefineOrEndEdge } from '.'; +import { mockAttackDiscoveries } from '../../../../evaluation/__mocks__/mock_attack_discoveries'; +import { + mockAnonymizedAlerts, + mockAnonymizedAlertsReplacements, +} from '../../../../evaluation/__mocks__/mock_anonymized_alerts'; +import { GraphState } from '../../types'; + +const logger = loggerMock.create(); + +const initialGraphState: GraphState = { + attackDiscoveries: null, // <-- no refined results + attackDiscoveryPrompt: 'prompt', + anonymizedAlerts: [...mockAnonymizedAlerts], + combinedGenerations: 'generations', + combinedRefinements: '', + errors: [], + generationAttempts: 2, + generations: ['gen', 'erations'], + hallucinationFailures: 0, + maxGenerationAttempts: 10, + maxHallucinationFailures: 5, + maxRepeatedGenerations: 3, + refinements: [], + refinePrompt: 'refinePrompt', + replacements: { + ...mockAnonymizedAlertsReplacements, + }, + unrefinedResults: mockAttackDiscoveries, +}; + +describe('getRefineOrEndEdge', () => { + beforeEach(() => jest.clearAllMocks()); + + it("returns 'end' when the refined results were generated", () => { + const state: GraphState = { + ...initialGraphState, + attackDiscoveries: mockAttackDiscoveries, // <-- attackDiscoveries are NOT null + }; + + const edge = getRefineOrEndEdge(logger); + const result = edge(state); + + expect(result).toEqual('end'); + }); + + it("returns 'refine' when there are unrefined results, and limits have NOT been reached", () => { + const edge = getRefineOrEndEdge(logger); + const result = edge(initialGraphState); + + expect(result).toEqual('refine'); + }); + + it("returns 'end' when the max generation attempts limit was reached", () => { + const state: GraphState = { + ...initialGraphState, + generationAttempts: initialGraphState.maxGenerationAttempts, + }; + + const edge = getRefineOrEndEdge(logger); + const result = edge(state); + + expect(result).toEqual('end'); + }); + + it("returns 'end' when the max hallucination failures limit was reached", () => { + const state: GraphState = { + ...initialGraphState, + hallucinationFailures: initialGraphState.maxHallucinationFailures, + }; + + const edge = getRefineOrEndEdge(logger); + const result = edge(state); + + expect(result).toEqual('end'); + }); + + it("returns 'end' when multiple limits are reached", () => { + const state: GraphState = { + ...initialGraphState, + generationAttempts: initialGraphState.maxGenerationAttempts, + hallucinationFailures: initialGraphState.maxHallucinationFailures, + }; + + const edge = getRefineOrEndEdge(logger); + const result = edge(state); + + expect(result).toEqual('end'); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/get_retrieve_or_generate/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/get_retrieve_or_generate/index.test.ts new file mode 100644 index 0000000000000..61dba4fb3d479 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/get_retrieve_or_generate/index.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRetrieveOrGenerate } from '.'; +import { mockAnonymizedAlerts } from '../../../../../evaluation/__mocks__/mock_anonymized_alerts'; + +describe('getRetrieveOrGenerate', () => { + it("returns 'retrieve_anonymized_alerts' when anonymizedAlerts is empty", () => { + expect(getRetrieveOrGenerate([])).toBe('retrieve_anonymized_alerts'); + }); + + it("returns 'generate' when anonymizedAlerts is not empty", () => { + expect(getRetrieveOrGenerate(mockAnonymizedAlerts)).toBe('generate'); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/index.test.ts new file mode 100644 index 0000000000000..06377aa565a12 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/index.test.ts @@ -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 { loggerMock } from '@kbn/logging-mocks'; + +import { getRetrieveAnonymizedAlertsOrGenerateEdge } from '.'; +import { mockAnonymizedAlerts } from '../../../../evaluation/__mocks__/mock_anonymized_alerts'; +import type { GraphState } from '../../types'; + +const logger = loggerMock.create(); + +const initialGraphState: GraphState = { + attackDiscoveries: null, + attackDiscoveryPrompt: 'prompt', + anonymizedAlerts: [], + combinedGenerations: '', + combinedRefinements: '', + errors: [], + generationAttempts: 0, + generations: [], + hallucinationFailures: 0, + maxGenerationAttempts: 10, + maxHallucinationFailures: 5, + maxRepeatedGenerations: 3, + refinements: [], + refinePrompt: 'refinePrompt', + replacements: {}, + unrefinedResults: null, +}; + +describe('getRetrieveAnonymizedAlertsOrGenerateEdge', () => { + beforeEach(() => jest.clearAllMocks()); + + it('returns "generate" when anonymizedAlerts is NOT empty, so there are alerts for the generate step', () => { + const state: GraphState = { + ...initialGraphState, + anonymizedAlerts: mockAnonymizedAlerts, + }; + + const edge = getRetrieveAnonymizedAlertsOrGenerateEdge(logger); + const result = edge(state); + + expect(result).toEqual('generate'); + }); + + it('returns "retrieve_anonymized_alerts" when anonymizedAlerts is empty, so they can be retrieved', () => { + const state: GraphState = { + ...initialGraphState, + anonymizedAlerts: [], // <-- empty + }; + + const edge = getRetrieveAnonymizedAlertsOrGenerateEdge(logger); + const result = edge(state); + + expect(result).toEqual('retrieve_anonymized_alerts'); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_hallucination_failures_reached/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_hallucination_failures_reached/index.test.ts new file mode 100644 index 0000000000000..138179109708e --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_hallucination_failures_reached/index.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMaxHallucinationFailuresReached } from '.'; + +describe('getMaxHallucinationFailuresReached', () => { + it('return true when hallucination failures is equal to the max hallucination failures', () => { + expect( + getMaxHallucinationFailuresReached({ hallucinationFailures: 2, maxHallucinationFailures: 2 }) + ).toBe(true); + }); + + it('returns true when hallucination failures is greater than the max hallucination failures', () => { + expect( + getMaxHallucinationFailuresReached({ hallucinationFailures: 3, maxHallucinationFailures: 2 }) + ).toBe(true); + }); + + it('returns false when hallucination failures is less than the max hallucination failures', () => { + expect( + getMaxHallucinationFailuresReached({ hallucinationFailures: 1, maxHallucinationFailures: 2 }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_retries_reached/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_retries_reached/index.test.ts new file mode 100644 index 0000000000000..47f49a75415c9 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_retries_reached/index.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { getMaxRetriesReached } from '.'; + +describe('getMaxRetriesReached', () => { + it('returns true when generation attempts is equal to the max generation attempts', () => { + expect(getMaxRetriesReached({ generationAttempts: 2, maxGenerationAttempts: 2 })).toBe(true); + }); + + it('returns true when generation attempts is greater than the max generation attempts', () => { + expect(getMaxRetriesReached({ generationAttempts: 3, maxGenerationAttempts: 2 })).toBe(true); + }); + + it('returns false when generation attempts is less than the max generation attempts', () => { + expect(getMaxRetriesReached({ generationAttempts: 1, maxGenerationAttempts: 2 })).toBe(false); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/discard_previous_generations/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/discard_previous_generations/index.test.ts new file mode 100644 index 0000000000000..6f3b3b1b909a2 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/discard_previous_generations/index.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { discardPreviousGenerations } from '.'; +import { GraphState } from '../../../../types'; + +const graphState: GraphState = { + attackDiscoveries: null, + attackDiscoveryPrompt: 'prompt', + anonymizedAlerts: [ + { + metadata: {}, + pageContent: + '@timestamp,2024-10-10T21:01:24.148Z\n' + + '_id,e809ffc5e0c2e731c1f146e0f74250078136a87574534bf8e9ee55445894f7fc\n' + + 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + + 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', + }, + { + metadata: {}, + pageContent: + '@timestamp,2024-10-10T21:01:24.148Z\n' + + '_id,c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b\n' + + 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + + 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', + }, + ], + combinedGenerations: 'combinedGenerations', + combinedRefinements: '', + errors: [], + generationAttempts: 2, + generations: ['combined', 'Generations'], + hallucinationFailures: 0, + maxGenerationAttempts: 10, + maxHallucinationFailures: 5, + maxRepeatedGenerations: 3, + refinements: [], + refinePrompt: 'refinePrompt', + replacements: {}, + unrefinedResults: null, +}; + +describe('discardPreviousGenerations', () => { + describe('common state updates', () => { + let result: GraphState; + + beforeEach(() => { + result = discardPreviousGenerations({ + generationAttempts: graphState.generationAttempts, + hallucinationFailures: graphState.hallucinationFailures, + isHallucinationDetected: false, + state: graphState, + }); + }); + + it('resets the combined generations', () => { + expect(result.combinedGenerations).toBe(''); + }); + + it('increments the generation attempts', () => { + expect(result.generationAttempts).toBe(graphState.generationAttempts + 1); + }); + + it('resets the collection of generations', () => { + expect(result.generations).toEqual([]); + }); + }); + + it('increments hallucinationFailures when a hallucination is detected', () => { + const result = discardPreviousGenerations({ + generationAttempts: graphState.generationAttempts, + hallucinationFailures: graphState.hallucinationFailures, + isHallucinationDetected: true, // <-- hallucination detected + state: graphState, + }); + + expect(result.hallucinationFailures).toBe(graphState.hallucinationFailures + 1); + }); + + it('does NOT increment hallucinationFailures when a hallucination is NOT detected', () => { + const result = discardPreviousGenerations({ + generationAttempts: graphState.generationAttempts, + hallucinationFailures: graphState.hallucinationFailures, + isHallucinationDetected: false, // <-- no hallucination detected + state: graphState, + }); + + expect(result.hallucinationFailures).toBe(graphState.hallucinationFailures); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_anonymized_alerts_from_state/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_anonymized_alerts_from_state/index.test.ts new file mode 100644 index 0000000000000..fb3d541e670df --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_anonymized_alerts_from_state/index.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getAnonymizedAlertsFromState } from '.'; + +import { mockAnonymizedAlerts } from '../../../../../../evaluation/__mocks__/mock_anonymized_alerts'; +import { GraphState } from '../../../../types'; + +const graphState: GraphState = { + attackDiscoveries: null, + attackDiscoveryPrompt: 'prompt', + anonymizedAlerts: mockAnonymizedAlerts, // <-- mockAnonymizedAlerts is an array of objects with a pageContent property + combinedGenerations: 'combinedGenerations', + combinedRefinements: '', + errors: [], + generationAttempts: 2, + generations: ['combined', 'Generations'], + hallucinationFailures: 0, + maxGenerationAttempts: 10, + maxHallucinationFailures: 5, + maxRepeatedGenerations: 3, + refinements: [], + refinePrompt: 'refinePrompt', + replacements: {}, + unrefinedResults: null, +}; + +describe('getAnonymizedAlertsFromState', () => { + it('returns the anonymized alerts from the state', () => { + const result = getAnonymizedAlertsFromState(graphState); + + expect(result).toEqual([ + mockAnonymizedAlerts[0].pageContent, + mockAnonymizedAlerts[1].pageContent, + ]); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_use_unrefined_results/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_use_unrefined_results/index.test.ts new file mode 100644 index 0000000000000..e9a75d7feb338 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_use_unrefined_results/index.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getUseUnrefinedResults } from '.'; +import { mockAttackDiscoveries } from '../../../../../../evaluation/__mocks__/mock_attack_discoveries'; + +describe('getUseUnrefinedResults', () => { + it('returns true when the next attempt would exceed the limit, and we have unrefined results', () => { + expect( + getUseUnrefinedResults({ + generationAttempts: 2, + maxGenerationAttempts: 3, + unrefinedResults: mockAttackDiscoveries, + }) + ).toBe(true); + }); + + it('returns false when the next attempt would NOT exceed the limit', () => { + expect( + getUseUnrefinedResults({ + generationAttempts: 1, + maxGenerationAttempts: 3, + unrefinedResults: mockAttackDiscoveries, + }) + ).toBe(false); + }); + + it('returns false when unrefined results is null', () => { + expect( + getUseUnrefinedResults({ + generationAttempts: 2, + maxGenerationAttempts: 3, + unrefinedResults: null, + }) + ).toBe(false); + }); + + it('returns false when unrefined results is empty', () => { + expect( + getUseUnrefinedResults({ + generationAttempts: 2, + maxGenerationAttempts: 3, + unrefinedResults: [], + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/index.test.ts new file mode 100644 index 0000000000000..da815aad9795a --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/index.test.ts @@ -0,0 +1,103 @@ +/* + * 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 { ActionsClientLlm } from '@kbn/langchain/server'; +import { loggerMock } from '@kbn/logging-mocks'; +import { FakeLLM } from '@langchain/core/utils/testing'; + +import { getGenerateNode } from '.'; +import { + mockAnonymizedAlerts, + mockAnonymizedAlertsReplacements, +} from '../../../../evaluation/__mocks__/mock_anonymized_alerts'; +import { getAnonymizedAlertsFromState } from './helpers/get_anonymized_alerts_from_state'; +import { getChainWithFormatInstructions } from '../helpers/get_chain_with_format_instructions'; +import { GraphState } from '../../types'; + +jest.mock('../helpers/get_chain_with_format_instructions', () => { + const mockInvoke = jest.fn().mockResolvedValue(''); + + return { + getChainWithFormatInstructions: jest.fn().mockReturnValue({ + chain: { + invoke: mockInvoke, + }, + formatInstructions: ['mock format instructions'], + llmType: 'fake', + mockInvoke, // <-- added for testing + }), + }; +}); + +const mockLogger = loggerMock.create(); +let mockLlm: ActionsClientLlm; + +const initialGraphState: GraphState = { + attackDiscoveries: null, + attackDiscoveryPrompt: + "You are a cyber security analyst tasked with analyzing security events from Elastic Security to identify and report on potential cyber attacks or progressions. Your report should focus on high-risk incidents that could severely impact the organization, rather than isolated alerts. Present your findings in a way that can be easily understood by anyone, regardless of their technical expertise, as if you were briefing the CISO. Break down your response into sections based on timing, hosts, and users involved. When correlating alerts, use kibana.alert.original_time when it's available, otherwise use @timestamp. Include appropriate context about the affected hosts and users. Describe how the attack progression might have occurred and, if feasible, attribute it to known threat groups. Prioritize high and critical alerts, but include lower-severity alerts if desired. In the description field, provide as much detail as possible, in a bulleted list explaining any attack progressions. Accuracy is of utmost importance. You MUST escape all JSON special characters (i.e. backslashes, double quotes, newlines, tabs, carriage returns, backspaces, and form feeds).", + anonymizedAlerts: [...mockAnonymizedAlerts], + combinedGenerations: '', + combinedRefinements: '', + errors: [], + generationAttempts: 0, + generations: [], + hallucinationFailures: 0, + maxGenerationAttempts: 10, + maxHallucinationFailures: 5, + maxRepeatedGenerations: 3, + refinements: [], + refinePrompt: + 'You previously generated the following insights, but sometimes they represent the same attack.\n\nCombine the insights below, when they represent the same attack; leave any insights that are not combined unchanged:', + replacements: { + ...mockAnonymizedAlertsReplacements, + }, + unrefinedResults: null, +}; + +describe('getGenerateNode', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockLlm = new FakeLLM({ + response: JSON.stringify({}, null, 2), + }) as unknown as ActionsClientLlm; + }); + + it('returns a function', () => { + const generateNode = getGenerateNode({ + llm: mockLlm, + logger: mockLogger, + }); + + expect(typeof generateNode).toBe('function'); + }); + + it('invokes the chain with the alerts from state and format instructions', async () => { + // @ts-expect-error + const { mockInvoke } = getChainWithFormatInstructions(mockLlm); + + const generateNode = getGenerateNode({ + llm: mockLlm, + logger: mockLogger, + }); + + await generateNode(initialGraphState); + + expect(mockInvoke).toHaveBeenCalledWith({ + format_instructions: ['mock format instructions'], + query: `${initialGraphState.attackDiscoveryPrompt} + +Use context from the following alerts to provide insights: + +\"\"\" +${getAnonymizedAlertsFromState(initialGraphState).join('\n\n')} +\"\"\" +`, + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_chain_with_format_instructions/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_chain_with_format_instructions/index.test.ts new file mode 100644 index 0000000000000..fd98af61150b8 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_chain_with_format_instructions/index.test.ts @@ -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 { FakeLLM } from '@langchain/core/utils/testing'; +import type { ActionsClientLlm } from '@kbn/langchain/server'; + +import { getChainWithFormatInstructions } from '.'; + +describe('getChainWithFormatInstructions', () => { + const mockLlm = new FakeLLM({ + response: JSON.stringify({}, null, 2), + }) as unknown as ActionsClientLlm; + + it('returns the chain with format instructions', () => { + const expectedFormatInstructions = `You must format your output as a JSON value that adheres to a given "JSON Schema" instance. + +"JSON Schema" is a declarative language that allows you to annotate and validate JSON documents. + +For example, the example "JSON Schema" instance {{"properties": {{"foo": {{"description": "a list of test words", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}}} +would match an object with one required property, "foo". The "type" property specifies "foo" must be an "array", and the "description" property semantically describes it as "a list of test words". The items within "foo" must be strings. +Thus, the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of this example "JSON Schema". The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted. + +Your output will be parsed and type-checked according to the provided schema instance, so make sure all fields in your output match the schema exactly and there are no trailing commas! + +Here is the JSON Schema instance your output must adhere to. Include the enclosing markdown codeblock: +\`\`\`json +{"type":"object","properties":{"insights":{"type":"array","items":{"type":"object","properties":{"alertIds":{"type":"array","items":{"type":"string"},"description":"The alert IDs that the insight is based on."},"detailsMarkdown":{"type":"string","description":"A detailed insight with markdown, where each markdown bullet contains a description of what happened that reads like a story of the attack as it played out and always uses special {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax for field names and values from the source data. Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }} Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}"},"entitySummaryMarkdown":{"type":"string","description":"A short (no more than a sentence) summary of the insight featuring only the host.name and user.name fields (when they are applicable), using the same {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax"},"mitreAttackTactics":{"type":"array","items":{"type":"string"},"description":"An array of MITRE ATT&CK tactic for the insight, using one of the following values: Reconnaissance,Initial Access,Execution,Persistence,Privilege Escalation,Discovery,Lateral Movement,Command and Control,Exfiltration"},"summaryMarkdown":{"type":"string","description":"A markdown summary of insight, using the same {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax"},"title":{"type":"string","description":"A short, no more than 7 words, title for the insight, NOT formatted with special syntax or markdown. This must be as brief as possible."}},"required":["alertIds","detailsMarkdown","summaryMarkdown","title"],"additionalProperties":false},"description":"Insights with markdown that always uses special {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax for field names and values from the source data. Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }} Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}"}},"required":["insights"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"} +\`\`\` +`; + + const chainWithFormatInstructions = getChainWithFormatInstructions(mockLlm); + expect(chainWithFormatInstructions).toEqual({ + chain: expect.any(Object), + formatInstructions: expectedFormatInstructions, + llmType: 'fake', + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined_attack_discovery_prompt/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined_attack_discovery_prompt/index.test.ts new file mode 100644 index 0000000000000..069dd77bed874 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined_attack_discovery_prompt/index.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 { getCombinedAttackDiscoveryPrompt } from '.'; + +describe('getCombinedAttackDiscoveryPrompt', () => { + it('returns the initial query when there are no partial results', () => { + const result = getCombinedAttackDiscoveryPrompt({ + anonymizedAlerts: ['alert1', 'alert2'], + attackDiscoveryPrompt: 'attackDiscoveryPrompt', + combinedMaybePartialResults: '', + }); + + expect(result).toBe(`attackDiscoveryPrompt + +Use context from the following alerts to provide insights: + +""" +alert1 + +alert2 +""" +`); + }); + + it('returns the initial query combined with a continuation prompt and partial results', () => { + const result = getCombinedAttackDiscoveryPrompt({ + anonymizedAlerts: ['alert1', 'alert2'], + attackDiscoveryPrompt: 'attackDiscoveryPrompt', + combinedMaybePartialResults: 'partialResults', + }); + + expect(result).toBe(`attackDiscoveryPrompt + +Use context from the following alerts to provide insights: + +""" +alert1 + +alert2 +""" + + +Continue exactly where you left off in the JSON output below, generating only the additional JSON output when it's required to complete your work. The additional JSON output MUST ALWAYS follow these rules: +1) it MUST conform to the schema above, because it will be checked against the JSON schema +2) it MUST escape all JSON special characters (i.e. backslashes, double quotes, newlines, tabs, carriage returns, backspaces, and form feeds), because it will be parsed as JSON +3) it MUST NOT repeat any the previous output, because that would prevent partial results from being combined +4) it MUST NOT restart from the beginning, because that would prevent partial results from being combined +5) it MUST NOT be prefixed or suffixed with additional text outside of the JSON, because that would prevent it from being combined and parsed as JSON: + + +""" +partialResults +""" + +`); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/response_is_hallucinated/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/response_is_hallucinated/index.test.ts new file mode 100644 index 0000000000000..3730d6a7c4b96 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/response_is_hallucinated/index.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { responseIsHallucinated } from '.'; + +describe('responseIsHallucinated', () => { + it('returns true when the response is hallucinated', () => { + expect( + responseIsHallucinated( + 'tactics like **Credential Access**, **Command and Control**, and **Persistence**.",\n "entitySummaryMarkdown": "Malware detected on host **{{ host.name hostNameValue }}**' + ) + ).toBe(true); + }); + + it('returns false when the response is not hallucinated', () => { + expect( + responseIsHallucinated( + 'A malicious file {{ file.name WsmpRExIFs.dll }} was detected on {{ host.name 082a86fa-b87d-45ce-813e-eed6b36ef0a9 }}\\n- The file was executed by' + ) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx index e8b6924eb15b4..7a36c6a973ec2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/ingestion_selector.tsx @@ -89,8 +89,7 @@ export const IngestionSelector: React.FC = () => { : i18n.translate( 'xpack.enterpriseSearch.ingestSelector.method.crawler.description', { - defaultMessage: - 'Discover, extract, and index searchable content from websites and knowledge bases.', + defaultMessage: 'Crawl URL', } ) } diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index d1dbeb9d35b64..ffb9381f8b30c 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -101,7 +101,6 @@ import { migratePackagePolicySetRequiresRootToV8150, } from './migrations/to_v8_15_0'; import { backfillAgentPolicyToV4 } from './model_versions/agent_policy_v4'; -import { packagePolicyV15AdvancedFieldsForEndpointV816 } from './model_versions/security_solution/v15_advanced_package_policy_fields'; /* * Saved object types and mappings @@ -751,14 +750,6 @@ export const getSavedObjectTypes = ( }, ], }, - '15': { - changes: [ - { - type: 'data_backfill', - backfillFn: packagePolicyV15AdvancedFieldsForEndpointV816, - }, - ], - }, }, migrations: { '7.10.0': migratePackagePolicyToV7100, diff --git a/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v15_advanced_package_policy_fields.test.ts b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v15_advanced_package_policy_fields.test.ts deleted file mode 100644 index 795c05966f53b..0000000000000 --- a/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v15_advanced_package_policy_fields.test.ts +++ /dev/null @@ -1,157 +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 { SavedObject } from '@kbn/core-saved-objects-api-server'; -import type { ModelVersionTestMigrator } from '@kbn/core-test-helpers-model-versions'; -import { createModelVersionTestMigrator } from '@kbn/core-test-helpers-model-versions'; -import { set } from '@kbn/safer-lodash-set'; - -import { getSavedObjectTypes } from '../..'; - -import type { PackagePolicy } from '../../../../common'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common'; - -describe('Defend integration advanced policy fields v8.16.0', () => { - const TARGET_MODEL_VERSION = 15; - - let migrator: ModelVersionTestMigrator; - let policyConfigSO: SavedObject; - - beforeEach(() => { - migrator = createModelVersionTestMigrator({ - type: getSavedObjectTypes()[PACKAGE_POLICY_SAVED_OBJECT_TYPE], - }); - - policyConfigSO = { - id: 'mock-saved-object-id', - attributes: { - name: 'Some Policy Name', - package: { - name: 'endpoint', - title: '', - version: '', - }, - id: 'endpoint', - policy_id: '', - policy_ids: [], - enabled: true, - namespace: '', - revision: 0, - updated_at: '', - updated_by: '', - created_at: '', - created_by: '', - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - policy: { - value: { - windows: {}, - mac: {}, - linux: {}, - }, - }, - }, - }, - ], - }, - type: PACKAGE_POLICY_SAVED_OBJECT_TYPE, - references: [], - }; - }); - - /** Builds object key paths for all parent objects - * - * @param path e.g. `advanced.events.optionName` - * @returns e.g. ['advanced', 'advanced.events'] - */ - const getParentObjectKeyPaths = (path: string): string[] => - path - .split('.') // ['advanced', 'events', 'optionName'] - .slice(0, -1) // ['advanced', 'events'] - .map((parentObject) => path.match(`^.*${parentObject}`)![0]); // ['advanced', 'advanced.events'] - - describe(`when updating to model version ${TARGET_MODEL_VERSION}`, () => { - describe.each` - name | path | backfill - ${'aggregate_process'} | ${'advanced.events.aggregate_process'} | ${false} - ${'set_extended_host_information'} | ${'advanced.set_extended_host_information'} | ${true} - ${'alerts.hash.md5'} | ${'advanced.alerts.hash.md5'} | ${true} - ${'alerts.hash.sha1'} | ${'advanced.alerts.hash.sha1'} | ${true} - ${'events.hash.md5'} | ${'advanced.events.hash.md5'} | ${true} - ${'events.hash.sha1'} | ${'advanced.events.hash.sha1'} | ${true} - `( - 'backfilling `$name` with `$backfill`', - ({ path, backfill }: { path: string; backfill: boolean }) => { - it('should backfill when there are no advanced options yet', () => { - const migratedPolicyConfigSO = migrator.migrate({ - document: policyConfigSO, - fromVersion: TARGET_MODEL_VERSION - 1, - toVersion: TARGET_MODEL_VERSION, - }); - - const migratedPolicyConfig = getConfig(migratedPolicyConfigSO); - - expectConfigToHave(migratedPolicyConfig, path, backfill); - }); - - it.each(getParentObjectKeyPaths(path))( - 'should backfill without modifying other options in parent object `%s`', - (parentObjectKeyPath) => { - const policyConfig = getConfig(policyConfigSO); - const dummyField = `${parentObjectKeyPath}.cheese`; - set(policyConfig.windows, dummyField, 'brie'); - set(policyConfig.mac, dummyField, 'maasdam'); - set(policyConfig.linux, dummyField, 'camambert'); - - const migratedPolicyConfigSO = migrator.migrate({ - document: policyConfigSO, - fromVersion: TARGET_MODEL_VERSION - 1, - toVersion: TARGET_MODEL_VERSION, - }); - - const migratedPolicyConfig = getConfig(migratedPolicyConfigSO); - - expectConfigToHave(migratedPolicyConfig, path, backfill); - expect(migratedPolicyConfig.windows).toHaveProperty(dummyField, 'brie'); - expect(migratedPolicyConfig.mac).toHaveProperty(dummyField, 'maasdam'); - expect(migratedPolicyConfig.linux).toHaveProperty(dummyField, 'camambert'); - } - ); - - it('should not backfill if field is already present', () => { - const policyConfig = getConfig(policyConfigSO); - set(policyConfig.windows, path, !backfill); - set(policyConfig.mac, path, !backfill); - set(policyConfig.linux, path, !backfill); - - const migratedPolicyConfigSO = migrator.migrate({ - document: policyConfigSO, - fromVersion: TARGET_MODEL_VERSION - 1, - toVersion: TARGET_MODEL_VERSION, - }); - - const migratedPolicyConfig = getConfig(migratedPolicyConfigSO); - - expectConfigToHave(migratedPolicyConfig, path, !backfill); - }); - } - ); - }); - - const getConfig = (so: SavedObject) => - so.attributes.inputs[0].config?.policy.value; - - const expectConfigToHave = (config: any, path: string, value: string | boolean) => { - for (const os of ['windows', 'mac', 'linux']) { - expect(config[os]).toHaveProperty(path, value); - } - }; -}); diff --git a/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v15_advanced_package_policy_fields.ts b/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v15_advanced_package_policy_fields.ts deleted file mode 100644 index 1fa21f3c94c1a..0000000000000 --- a/x-pack/plugins/fleet/server/saved_objects/model_versions/security_solution/v15_advanced_package_policy_fields.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - SavedObjectModelDataBackfillFn, - SavedObjectUnsanitizedDoc, -} from '@kbn/core-saved-objects-server'; - -import type { PackagePolicy } from '../../../../common'; - -export const packagePolicyV15AdvancedFieldsForEndpointV816: SavedObjectModelDataBackfillFn< - PackagePolicy, - PackagePolicy -> = (packagePolicyDoc) => { - if (packagePolicyDoc.attributes.package?.name !== 'endpoint') { - return { attributes: packagePolicyDoc.attributes }; - } - - const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = packagePolicyDoc; - - const input = updatedPackagePolicyDoc.attributes.inputs[0]; - - if (input && input.config) { - const policy = input.config.policy.value; - - for (const os of ['windows', 'mac', 'linux']) { - const policyPerOs = policy[os]; - - policyPerOs.advanced = { - set_extended_host_information: true, - ...policyPerOs.advanced, - - events: { - aggregate_process: false, - ...policyPerOs.advanced?.events, - - hash: { - md5: true, - sha1: true, - ...policyPerOs.advanced?.events?.hash, - }, - }, - - alerts: { - ...policyPerOs.advanced?.alerts, - - hash: { - md5: true, - sha1: true, - ...policyPerOs.advanced?.alerts?.hash, - }, - }, - }; - } - } - - return { attributes: updatedPackagePolicyDoc.attributes }; -}; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts index e21ae1e06dc78..3681d93e8707d 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.test.ts @@ -53,7 +53,10 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn( ); describe('output preconfiguration', () => { + let logstashSecretHash: string; + beforeEach(async () => { + logstashSecretHash = await hashSecret('secretKey'); const internalSoClientWithoutSpaceExtension = savedObjectsClientMock.create(); jest .mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension) @@ -120,7 +123,7 @@ describe('output preconfiguration', () => { id: 'existing-logstash-output-with-secrets-2', is_default: false, is_default_monitoring: false, - name: 'Logstash Output With Secrets 2', + name: 'Logstash Output With Secrets ', type: 'logstash', hosts: ['test:4343'], is_preconfigured: true, @@ -130,6 +133,34 @@ describe('output preconfiguration', () => { }, }, }, + { + id: 'existing-logstash-output-with-secrets-3-outdatded-hash', + is_default: false, + is_default_monitoring: false, + name: 'Logstash Output With Secrets 3', + type: 'logstash', + hosts: ['test:4343'], + is_preconfigured: true, + secrets: { + ssl: { + key: { id: 'test456', hash: 'test456:outdatedhash' }, + }, + }, + }, + { + id: 'existing-logstash-output-with-secrets-4-hash', + is_default: false, + is_default_monitoring: false, + name: 'Logstash Output With Secrets 4', + type: 'logstash', + hosts: ['test:4343'], + is_preconfigured: true, + secrets: { + ssl: { + key: { id: 'test123', hash: logstashSecretHash }, + }, + }, + }, { id: 'existing-kafka-output-1', is_default: false, @@ -689,6 +720,56 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); }); + it('should update output if a preconfigured logstash output with secrets exists and hash algorithm changed', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await createOrUpdatePreconfiguredOutputs(soClient, esClient, [ + { + id: 'existing-logstash-output-with-secrets-3-outdatded-hash', + is_default: false, + is_default_monitoring: false, + name: 'Logstash Output With Secrets 3', + type: 'logstash', + hosts: ['test:4343'], + is_preconfigured: true, + secrets: { + ssl: { + key: 'secretKey', // no change + }, + }, + }, + ]); + + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); + }); + + it('should not update output if a preconfigured logstash output with secrets exists and hash algorithm did not changed', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + await createOrUpdatePreconfiguredOutputs(soClient, esClient, [ + { + id: 'existing-logstash-output-with-secrets-4-hash', + is_default: false, + is_default_monitoring: false, + name: 'Logstash Output With Secrets 4', + type: 'logstash', + hosts: ['test:4343'], + is_preconfigured: true, + secrets: { + ssl: { + key: 'secretKey', // no change + }, + }, + }, + ]); + + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).not.toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); + }); + it('should update output if a preconfigured kafka output with plain value secrets exists and did not change', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts index fa7103bebb3b4..cff6deb6a24a3 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts @@ -5,12 +5,15 @@ * 2.0. */ -import crypto from 'crypto'; +import crypto from 'node:crypto'; +import utils from 'node:util'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { isEqual } from 'lodash'; import { safeDump } from 'js-yaml'; +const pbkdf2Async = utils.promisify(crypto.pbkdf2); + import type { PreconfiguredOutput, Output, @@ -142,32 +145,23 @@ export async function createOrUpdatePreconfiguredOutputs( // Values recommended by NodeJS documentation const keyLength = 64; const saltLength = 16; - -// N=2^14 (16 MiB), r=8 (1024 bytes), p=5 -const scryptParams = { - cost: 16384, - blockSize: 8, - parallelization: 5, -}; +const maxIteration = 100000; export async function hashSecret(secret: string) { - return new Promise((resolve, reject) => { - const salt = crypto.randomBytes(saltLength).toString('hex'); - crypto.scrypt(secret, salt, keyLength, scryptParams, (err, derivedKey) => { - if (err) reject(err); - resolve(`${salt}:${derivedKey.toString('hex')}`); - }); - }); + const salt = crypto.randomBytes(saltLength).toString('hex'); + const derivedKey = await pbkdf2Async(secret, salt, maxIteration, keyLength, 'sha512'); + + return `${salt}:${derivedKey.toString('hex')}`; } async function verifySecret(hash: string, secret: string) { - return new Promise((resolve, reject) => { - const [salt, key] = hash.split(':'); - crypto.scrypt(secret, salt, keyLength, scryptParams, (err, derivedKey) => { - if (err) reject(err); - resolve(crypto.timingSafeEqual(Buffer.from(key, 'hex'), derivedKey)); - }); - }); + const [salt, key] = hash.split(':'); + const derivedKey = await pbkdf2Async(secret, salt, maxIteration, keyLength, 'sha512'); + const keyBuffer = Buffer.from(key, 'hex'); + if (keyBuffer.length !== derivedKey.length) { + return false; + } + return crypto.timingSafeEqual(Buffer.from(key, 'hex'), derivedKey); } async function hashSecrets(output: PreconfiguredOutput) { diff --git a/x-pack/plugins/graph/public/helpers/kql_encoder.ts b/x-pack/plugins/graph/public/helpers/kql_encoder.ts index 25ac6b9c4280d..e1eeb0f2dc35b 100644 --- a/x-pack/plugins/graph/public/helpers/kql_encoder.ts +++ b/x-pack/plugins/graph/public/helpers/kql_encoder.ts @@ -10,7 +10,7 @@ import rison from '@kbn/rison'; import { Workspace } from '../types'; function escapeQuotes(str: string) { - return str.replace(/"/g, '\\"'); + return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } export function asKQL(workspace: Workspace, joinBy: 'and' | 'or') { diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/manage_processors.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/manage_processors.test.tsx index 81375d1e3ae83..c142a017d9bef 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/manage_processors.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/manage_processors.test.tsx @@ -73,7 +73,7 @@ describe('', () => { expect(row).toEqual([ database.name, - database.type === 'maxmind' ? 'MaxMind' : 'IPInfo', + database.type === 'maxmind' ? 'MaxMind' : 'IPinfo', '', ]); }); @@ -122,7 +122,7 @@ describe('', () => { }); }); - it('creates an IPInfo database when none with the same name exists', async () => { + it('creates an IPinfo database when none with the same name exists', async () => { const { actions, exists } = testBed; const databaseName = 'ASN'; httpRequestsMockHelpers.setCreateDatabasesResponse({ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx index d1b8fbd7ea513..765c482f1c86b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx @@ -27,12 +27,14 @@ import { PropertiesField } from './common_fields/properties_field'; import type { GeoipDatabase } from '../../../../../../../common/types'; import { getTypeLabel } from '../../../../../sections/manage_processors/constants'; +const extension = '.mmdb'; + const fieldsConfig: FieldsConfig = { /* Optional field config */ database_file: { type: FIELD_TYPES.COMBO_BOX, - deserializer: to.arrayOfStrings, - serializer: (v: string[]) => (v.length ? v[0] : undefined), + deserializer: (v: unknown) => to.arrayOfStrings(v).map((str) => str?.split(extension)[0]), + serializer: (v: string[]) => (v.length ? `${v[0]}${extension}` : undefined), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.ipLocationForm.databaseFileLabel', { defaultMessage: 'Database file (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index f099e4938cd3f..06016d77c7365 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -467,7 +467,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }), typeDescription: i18n.translate('xpack.ingestPipelines.processors.description.geoip', { defaultMessage: - 'Adds geo data based on an IP address. Uses geo data from a Maxmind database file.', + 'Adds geo data based on an IP address. Uses geo data from a MaxMind database file.', }), getDefaultDescription: ({ field }) => i18n.translate('xpack.ingestPipelines.processors.defaultDescription.geoip', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/manage_processors/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/manage_processors/constants.ts index 799c3a8c29b40..63b201936d859 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/manage_processors/constants.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/manage_processors/constants.ts @@ -20,7 +20,7 @@ export const DATABASE_TYPE_OPTIONS = [ { value: 'ipinfo', text: i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.ipinfoDatabaseType', { - defaultMessage: 'IPInfo', + defaultMessage: 'IPinfo', }), }, ]; @@ -153,7 +153,7 @@ export const getTypeLabel = (type: GeoipDatabase['type']): string => { } case 'ipinfo': { return i18n.translate('xpack.ingestPipelines.manageProcessors.geoip.list.typeIpinfoLabel', { - defaultMessage: 'IPInfo', + defaultMessage: 'IPinfo', }); } case 'web': { diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx index a354b54fb37e9..aa6837a7e3393 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx @@ -373,7 +373,7 @@ describe('FormBasedDimensionEditor', () => { // // press arrow up to go back to the beginning await userEvent.type(comboBoxInput, '{ArrowUp}{ArrowUp}'); expect(getVisibleFieldSelectOptions()).toEqual(allOptions.slice(8)); - }); + }, 10000); // this test can be long running due to a big tree we're rendering and userEvent.type function that is slow it('should hide fields that have no data', () => { (useExistingFieldsReader as jest.Mock).mockImplementationOnce(() => { diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx index f4ef2044a38c7..3f05d872f6d1f 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx @@ -7,12 +7,13 @@ import { EuiFlexGroup, EuiPageHeaderProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { entityCentricExperience } from '@kbn/observability-plugin/common'; import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import React, { useContext } from 'react'; import { useLocation } from 'react-router-dom'; import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { isLogsSignal } from '../../../../utils/get_signal_type'; import { useLocalStorage } from '../../../../hooks/use_local_storage'; import { useDefaultAiAssistantStarterPromptsForAPM } from '../../../../hooks/use_default_ai_assistant_starter_prompts_for_apm'; import { KibanaEnvironmentContext } from '../../../../context/kibana_environment_context/kibana_environment_context'; @@ -71,12 +72,8 @@ export function ApmMainTemplate({ const { http, docLinks, observabilityShared, application } = services; const { kibanaVersion, isCloudEnv, isServerlessEnv } = kibanaEnvironment; const basePath = http?.basePath.get(); - const { config, core } = useApmPluginContext(); - const isEntityCentricExperienceSettingEnabled = core.uiSettings.get( - entityCentricExperience, - true - ); - + const { config } = useApmPluginContext(); + const { serviceEntitySummary } = useApmServiceContext(); const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting(); const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; @@ -97,9 +94,14 @@ export function ApmMainTemplate({ [application?.capabilities.savedObjectsManagement.edit] ); - const shouldBypassNoDataScreen = bypassNoDataScreenPaths.some((path) => - location.pathname.includes(path) - ); + const hasLogsData = serviceEntitySummary?.dataStreamTypes + ? serviceEntitySummary?.dataStreamTypes?.length > 0 && + isLogsSignal(serviceEntitySummary.dataStreamTypes) + : false; + + const shouldBypassNoDataScreen = + bypassNoDataScreenPaths.some((path) => location.pathname.includes(path)) || + (isEntityCentricExperienceEnabled && hasLogsData); const { data: fleetApmPoliciesData, status: fleetApmPoliciesStatus } = useFetcher( (callApmApi) => { @@ -158,7 +160,7 @@ export function ApmMainTemplate({ const showEntitiesInventoryCallout = !dismissedEntitiesInventoryCallout && - isEntityCentricExperienceSettingEnabled && + isEntityCentricExperienceEnabled && selectedNavButton !== undefined; return ( diff --git a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts index 3d6fa0f5a5ef6..8f201efbe6549 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/errors/get_error_groups/get_error_group_main_statistics.ts @@ -97,14 +97,10 @@ export async function getErrorGroupMainStatistics({ ] : []; - const requiredFields = asMutableArray([ - TRACE_ID, - AT_TIMESTAMP, - ERROR_GROUP_ID, - ERROR_ID, - ] as const); + const requiredFields = asMutableArray([AT_TIMESTAMP, ERROR_GROUP_ID, ERROR_ID] as const); const optionalFields = asMutableArray([ + TRACE_ID, ERROR_CULPRIT, ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index c6c68830ae10c..cf376e7c78294 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -103,7 +103,7 @@ export class APMEventClient { /** @deprecated Use {@link excludedDataTiers} instead. * See https://www.elastic.co/guide/en/kibana/current/advanced-options.html **/ private readonly includeFrozen: boolean; - private readonly excludedDataTiers?: DataTier[]; + private readonly excludedDataTiers: DataTier[]; private readonly inspectableEsQueriesMap?: WeakMap; constructor(config: APMEventClientConfig) { @@ -112,7 +112,7 @@ export class APMEventClient { this.request = config.request; this.indices = config.indices; this.includeFrozen = config.options.includeFrozen; - this.excludedDataTiers = config.options.excludedDataTiers; + this.excludedDataTiers = config.options.excludedDataTiers ?? []; this.inspectableEsQueriesMap = config.options.inspectableEsQueriesMap; } @@ -167,7 +167,7 @@ export class APMEventClient { indices: this.indices, }); - if (this.excludedDataTiers) { + if (this.excludedDataTiers.length > 0) { filters.push(...excludeTiersQuery(this.excludedDataTiers)); } @@ -207,7 +207,8 @@ export class APMEventClient { // Reusing indices configured for errors since both events and errors are stored as logs. const index = processorEventsToIndex([ProcessorEvent.error], this.indices); - const filter = this.excludedDataTiers ? excludeTiersQuery(this.excludedDataTiers) : undefined; + const filter = + this.excludedDataTiers.length > 0 ? excludeTiersQuery(this.excludedDataTiers) : undefined; const searchParams = { ...omit(params, 'body'), @@ -249,7 +250,7 @@ export class APMEventClient { indices: this.indices, }); - if (this.excludedDataTiers) { + if (this.excludedDataTiers.length > 0) { filters.push(...excludeTiersQuery(this.excludedDataTiers)); } diff --git a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts index ae29575c044c6..cad0b03579e3d 100644 --- a/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts +++ b/x-pack/plugins/observability_solution/apm_data_access/server/lib/helpers/tier_filter.ts @@ -13,10 +13,10 @@ export function getDataTierFilterCombined({ excludedDataTiers, }: { filter?: QueryDslQueryContainer; - excludedDataTiers?: DataTier[]; + excludedDataTiers: DataTier[]; }): QueryDslQueryContainer | undefined { if (!filter) { - return excludedDataTiers ? excludeTiersQuery(excludedDataTiers)[0] : undefined; + return excludedDataTiers.length > 0 ? excludeTiersQuery(excludedDataTiers)[0] : undefined; } return !excludedDataTiers diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.ts index 795700bfc9441..d3fad141335de 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.ts @@ -194,16 +194,20 @@ export const useDegradedDocsChart = () => { const extraActions: Action[] = [getOpenInLensAction, getOpenInLogsExplorerAction]; - return { - attributes, - dataView, - breakdown: { + const breakdown = useMemo(() => { + return { dataViewField: breakdownDataViewField, fieldSupportsBreakdown: breakdownDataViewField ? fieldSupportsBreakdown(breakdownDataViewField) : true, onChange: handleBreakdownFieldChange, - }, + }; + }, [breakdownDataViewField, handleBreakdownFieldChange]); + + return { + attributes, + dataView, + breakdown, extraActions, isChartLoading, onChartLoading: handleChartLoading, diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx index 363fc88c8a490..c6e8790eeff6a 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx @@ -8,6 +8,8 @@ import React, { useEffect } from 'react'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { useEntityCentricExperienceSetting } from '../../../hooks/use_entity_centric_experience_setting'; +import { isPending } from '../../../hooks/use_fetcher'; import { SYSTEM_INTEGRATION } from '../../../../common/constants'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { useParentBreadcrumbResolver } from '../../../hooks/use_parent_breadcrumb_resolver'; @@ -24,7 +26,7 @@ import { InfraPageTemplate } from '../../shared/templates/infra_page_template'; import { OnboardingFlow } from '../../shared/templates/no_data_config'; import { PageTitleWithPopover } from '../header/page_title_with_popover'; import { useEntitySummary } from '../hooks/use_entity_summary'; -import { isMetricsSignal } from '../utils/get_data_stream_types'; +import { isLogsSignal, isMetricsSignal } from '../utils/get_data_stream_types'; const DATA_AVAILABILITY_PER_TYPE: Partial> = { host: [SYSTEM_INTEGRATION], @@ -36,10 +38,11 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { const { rightSideItems, tabEntries, breadcrumbs: headerBreadcrumbs } = usePageHeader(tabs, links); const { asset } = useAssetDetailsRenderPropsContext(); const trackOnlyOnce = React.useRef(false); - const { dataStreams } = useEntitySummary({ + const { dataStreams, status: entitySummaryStatus } = useEntitySummary({ entityType: asset.type, entityId: asset.id, }); + const { isEntityCentricExperienceEnabled } = useEntityCentricExperienceSetting(); const { activeTabId } = useTabSwitcherContext(); const { services: { telemetry }, @@ -85,10 +88,18 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { }, [activeTabId, asset.type, metadata, metadataLoading, telemetry]); const showPageTitleWithPopover = asset.type === 'host' && !isMetricsSignal(dataStreams); + const shouldBypassOnboarding = + isEntityCentricExperienceEnabled && (isLogsSignal(dataStreams) || isMetricsSignal(dataStreams)); return ( & { dataAvailabilityModules?: string[]; @@ -41,14 +41,18 @@ export const InfraPageTemplate = ({ const { error: dataViewLoadError, refetch: loadDataView } = useMetricsDataViewContext(); const { remoteClustersExist } = source?.status ?? {}; - const { data, status } = useFetcher(async (callApi) => { - return await callApi('/api/metrics/source/hasData', { - method: 'GET', - query: { - modules: dataAvailabilityModules, - }, - }); - }); + const { data, status } = useFetcher( + async (callApi) => { + if (!onboardingFlow) return; + return await callApi('/api/metrics/source/hasData', { + method: 'GET', + query: { + modules: dataAvailabilityModules, + }, + }); + }, + [onboardingFlow, dataAvailabilityModules] + ); const hasData = !!data?.hasData; const noDataConfig = getNoDataConfig({ diff --git a/x-pack/plugins/observability_solution/infra/public/components/shared/templates/no_data_config.ts b/x-pack/plugins/observability_solution/infra/public/components/shared/templates/no_data_config.ts index faf53cccca4cd..3859f93ddc437 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/shared/templates/no_data_config.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/shared/templates/no_data_config.ts @@ -108,11 +108,11 @@ export const getNoDataConfig = ({ }: { hasData: boolean; loading: boolean; - onboardingFlow: OnboardingFlow; + onboardingFlow?: OnboardingFlow; locators: LocatorClient; docsLink?: string; }): NoDataConfig | undefined => { - if (hasData || loading) { + if (!onboardingFlow || hasData || loading) { return; } diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_entity_centric_experience_setting.tsx b/x-pack/plugins/observability_solution/infra/public/hooks/use_entity_centric_experience_setting.tsx new file mode 100644 index 0000000000000..1cbc4680405e4 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_entity_centric_experience_setting.tsx @@ -0,0 +1,17 @@ +/* + * 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 { entityCentricExperience } from '@kbn/observability-plugin/common'; +import { useKibanaContextForPlugin } from './use_kibana'; + +export function useEntityCentricExperienceSetting() { + const { uiSettings } = useKibanaContextForPlugin().services; + + const isEntityCentricExperienceEnabled = uiSettings.get(entityCentricExperience, true); + + return { isEntityCentricExperienceEnabled }; +} diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/index.tsx index c013cebb42e70..b3d82e0c8fc22 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { css } from '@emotion/react'; +import { OnboardingFlow } from '../../../components/shared/templates/no_data_config'; import { InfraPageTemplate } from '../../../components/shared/templates/infra_page_template'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { inventoryTitle } from '../../../translations'; @@ -38,6 +39,7 @@ export const SnapshotPage = () => {
, ], diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx index 3934abb5478f9..0de2ae8f6fbe7 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx @@ -11,6 +11,7 @@ import moment from 'moment'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { InventoryMetric, InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; import { decodeOrThrow } from '@kbn/io-ts-utils'; +import { OnboardingFlow } from '../../../../components/shared/templates/no_data_config'; import { InfraPageTemplate } from '../../../../components/shared/templates/infra_page_template'; import { NodeDetailsMetricDataResponseRT } from '../../../../../common/http_api/node_details_api'; import { isPending, useFetcher } from '../../../../hooks/use_fetcher'; @@ -91,6 +92,7 @@ export const NodeDetailsPage = (props: Props) => { return ( { if (metadataLoading && !filteredRequiredMetrics.length) { return ( - + { return ( { 'Monitor your host and the services running on it, set-up SLO, get alerted, remediate performance issues', } ), - logos: ['kubernetes', 'opentelemetry', 'apache', 'mysql'], + logos: ['opentelemetry', 'apache', 'mysql'], }, { id: 'kubernetes', @@ -86,7 +86,7 @@ export const OnboardingFlowForm: FunctionComponent = () => { 'Monitor the frontend and backend application that you have developed, set-up synthetic monitors', } ), - logos: ['opentelemetry', 'java', 'javascript', 'dotnet'], + logos: ['opentelemetry', 'java', 'ruby', 'dotnet'], }, { id: 'cloud', diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx index 4910f6de28904..10b2c52e441ee 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/use_custom_cards_for_category.tsx @@ -33,6 +33,10 @@ export function useCustomCardsForCategory( const { href: autoDetectUrl } = reactRouterNavigate(history, `/auto-detect/${location.search}`); const { href: otelLogsUrl } = reactRouterNavigate(history, `/otel-logs/${location.search}`); const { href: kubernetesUrl } = reactRouterNavigate(history, `/kubernetes/${location.search}`); + const { href: otelKubernetesUrl } = reactRouterNavigate( + history, + `/otel-kubernetes/${location.search}` + ); const apmUrl = `${getUrlForApp?.('apm')}/${isServerless ? 'onboarding' : 'tutorial'}`; const otelApmUrl = isServerless ? `${apmUrl}?agent=openTelemetry` : apmUrl; @@ -44,9 +48,14 @@ export function useCustomCardsForCategory( id: 'auto-detect-logs', name: 'auto-detect-logs-virtual', type: 'virtual', - title: 'Auto-detect Integrations with Elastic Agent', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.autoDetectTitle', + { + defaultMessage: 'Auto-detect Integrations with Elastic Agent', + } + ), description: i18n.translate( - 'xpack.observability_onboarding.useCustomCardsForCategory.scanYourHostForLabel', + 'xpack.observability_onboarding.useCustomCardsForCategory.autoDetectDescription', { defaultMessage: 'Scan your host for log and metric files, auto-install integrations', } @@ -75,8 +84,19 @@ export function useCustomCardsForCategory( id: 'otel-logs', name: 'custom-logs-virtual', type: 'virtual', - title: 'Elastic Distribution for OTel Collector', - description: 'Collect logs and host metrics using the Elastic Distro for OTel Collector ', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.logsOtelTitle', + { + defaultMessage: 'Host monitoring with EDOT Collector', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.logsOtelDescription', + { + defaultMessage: + 'Collect logs and host metrics with the Elastic Distro for OTel Collector', + } + ), extraLabelsBadges: [ @@ -105,8 +125,19 @@ export function useCustomCardsForCategory( id: 'kubernetes-quick-start', name: 'kubernetes-quick-start', type: 'virtual', - title: 'Elastic Agent', - description: 'Monitor your Kubernetes cluster with Elastic Agent, collect container logs', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesTitle', + { + defaultMessage: 'Kubernetes monitoring with Elastic Agent', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesDescription', + { + defaultMessage: + 'Monitor your Kubernetes cluster with Elastic Agent, collect container logs', + } + ), extraLabelsBadges: [ @@ -125,11 +156,22 @@ export function useCustomCardsForCategory( isQuickstart: true, }, { - id: 'otel-logs', - name: 'custom-logs-virtual', + id: 'otel-kubernetes', + name: 'otel-kubernetes-virtual', type: 'virtual', - title: 'Elastic Distribution for OTel Collector', - description: 'Collect logs, metrics and traces for Kubernetes cluster monitoring', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelTitle', + { + defaultMessage: 'Kubernetes monitoring with EDOT Collector', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.kubernetesOtelDescription', + { + defaultMessage: + 'Unified Kubernetes observability with Elastic Distro for OTel Collector', + } + ), extraLabelsBadges: [ @@ -142,9 +184,10 @@ export function useCustomCardsForCategory( src: http?.staticAssets.getPluginAssetHref('opentelemetry.svg') ?? '', }, ], - url: otelLogsUrl, + url: otelKubernetesUrl, version: '', integration: '', + isQuickstart: true, }, ]; @@ -153,8 +196,18 @@ export function useCustomCardsForCategory( { id: 'apm-virtual', type: 'virtual', - title: 'Elastic APM', - description: 'Collect distributed traces from your applications with Elastic APM', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.apmTitle', + { + defaultMessage: 'Elastic APM', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.apmDescription', + { + defaultMessage: 'Collect distributed traces from your applications with Elastic APM', + } + ), name: 'apm', categories: ['observability'], icons: [ @@ -170,8 +223,18 @@ export function useCustomCardsForCategory( { id: 'otel-virtual', type: 'virtual', - title: 'OpenTelemetry', - description: 'Collect distributed traces with OpenTelemetry', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.apmOtelTitle', + { + defaultMessage: 'OpenTelemetry', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.apmOtelDescription', + { + defaultMessage: 'Collect distributed traces with OpenTelemetry', + } + ), name: 'otel', categories: ['observability'], icons: [ @@ -187,8 +250,18 @@ export function useCustomCardsForCategory( { id: 'synthetics-virtual', type: 'virtual', - title: 'Synthetic monitor', - description: 'Monitor endpoints, pages, and user journeys', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.syntheticsTitle', + { + defaultMessage: 'Synthetic monitor', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.syntheticsDescription', + { + defaultMessage: 'Monitor endpoints, pages, and user journeys', + } + ), name: 'synthetics', categories: ['observability'], icons: [ @@ -208,8 +281,18 @@ export function useCustomCardsForCategory( { id: 'azure-logs-virtual', type: 'virtual', - title: 'Azure', - description: 'Collect logs from Microsoft Azure', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.azureTitle', + { + defaultMessage: 'Azure', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.azureDescription', + { + defaultMessage: 'Collect logs from Microsoft Azure', + } + ), name: 'azure', categories: ['observability'], icons: [], @@ -222,8 +305,18 @@ export function useCustomCardsForCategory( { id: 'aws-logs-virtual', type: 'virtual', - title: 'AWS', - description: 'Collect logs from Amazon Web Services (AWS)', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.awsTitle', + { + defaultMessage: 'AWS', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.awsDescription', + { + defaultMessage: 'Collect logs from Amazon Web Services (AWS)', + } + ), name: 'aws', categories: ['observability'], icons: [], @@ -236,8 +329,18 @@ export function useCustomCardsForCategory( { id: 'gcp-logs-virtual', type: 'virtual', - title: 'Google Cloud Platform', - description: 'Collect logs from Google Cloud Platform', + title: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.gcpTitle', + { + defaultMessage: 'Google Cloud Platform', + } + ), + description: i18n.translate( + 'xpack.observability_onboarding.useCustomCardsForCategory.gcpDescription', + { + defaultMessage: 'Collect logs from Google Cloud Platform', + } + ), name: 'gcp', categories: ['observability'], icons: [], diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx index c745793c47b3a..9d3e07cc2f612 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx @@ -30,6 +30,10 @@ import { CopyToClipboardButton } from '../shared/copy_to_clipboard_button'; import { ObservabilityOnboardingContextValue } from '../../../plugin'; import { useKubernetesFlow } from '../kubernetes/use_kubernetes_flow'; +const OTEL_HELM_CHARTS_REPO = 'https://open-telemetry.github.io/opentelemetry-helm-charts'; +const OTEL_KUBE_STACK_VERSION = '0.3.0'; +const OTEL_KUBE_STACK_VALUES_FILE_URL = + 'https://raw.githubusercontent.com/elastic/opentelemetry/refs/heads/main/resources/kubernetes/operator/helm/values.yaml'; const CLUSTER_OVERVIEW_DASHBOARD_ID = 'kubernetes_otel-cluster-overview'; export const OtelKubernetesPanel: React.FC = () => { @@ -48,10 +52,7 @@ export const OtelKubernetesPanel: React.FC = () => { } const namespace = 'opentelemetry-operator-system'; - const valuesFile = - 'https://raw.githubusercontent.com/elastic/opentelemetry/refs/heads/main/resources/kubernetes/operator/helm/values.yaml'; - - const addRepoCommand = `helm repo add open-telemetry 'https://open-telemetry.github.io/opentelemetry-helm-charts' --force-update`; + const addRepoCommand = `helm repo add open-telemetry '${OTEL_HELM_CHARTS_REPO}' --force-update`; const installStackCommand = data ? `kubectl create namespace ${namespace} kubectl create secret generic elastic-secret-otel \\ @@ -60,8 +61,8 @@ kubectl create secret generic elastic-secret-otel \\ --from-literal=elastic_api_key='${data.apiKeyEncoded}' helm install opentelemetry-kube-stack open-telemetry/opentelemetry-kube-stack \\ --namespace ${namespace} \\ - --create-namespace \\ - --values '${valuesFile}'` + --values '${OTEL_KUBE_STACK_VALUES_FILE_URL}' \\ + --version '${OTEL_KUBE_STACK_VERSION}'` : undefined; return ( @@ -143,7 +144,7 @@ helm install opentelemetry-kube-stack open-telemetry/opentelemetry-kube-stack \\ diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx index 9eb7c83b03fc0..a78466d06a6b2 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx @@ -88,458 +88,6 @@ export const OtelLogsPanel: React.FC = () => { }, [getDeeplinks]); const installTabContents = [ - { - id: 'kubernetes', - name: 'Kubernetes', - prompt: ( - <> - -

- {i18n.translate( - 'xpack.observability_onboarding.otelLogsPanel.kubernetesApplyCommandPromptLabel', - { - defaultMessage: - 'From the directory where the manifest is downloaded, run the following command to install the collector on every node of your cluster:', - } - )} -

-
- - - ), - firstStepTitle: i18n.translate( - 'xpack.observability_onboarding.otelLogsPanel.steps.downloadManifest', - { defaultMessage: 'Download the manifest:' } - ), - content: `apiVersion: v1 -kind: ServiceAccount -metadata: - name: elastic-otel-collector-agent - namespace: default - labels: - app.kubernetes.io/name: elastic-opentelemetry-collector - app.kubernetes.io/version: "${agentVersion}" ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: elastic-otel-collector-agent - labels: - app.kubernetes.io/name: elastic-opentelemetry-collector - app.kubernetes.io/version: "${agentVersion}" -rules: - - apiGroups: [""] - resources: ["pods", "namespaces", "nodes"] - verbs: ["get", "watch", "list"] - - apiGroups: ["apps"] - resources: ["daemonsets", "deployments", "replicasets", "statefulsets"] - verbs: ["get", "list", "watch"] - - apiGroups: ["extensions"] - resources: ["daemonsets", "deployments", "replicasets"] - verbs: ["get", "list", "watch"] - - apiGroups: [ "" ] - resources: [ "nodes/stats" ] - verbs: [ "get", "watch", "list" ] - - apiGroups: [ "" ] - resources: [ "nodes/proxy" ] - verbs: [ "get" ] - - apiGroups: [ "" ] - resources: ["configmaps"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: elastic-otel-collector-agent - labels: - app.kubernetes.io/name: elastic-opentelemetry-collector - app.kubernetes.io/version: "${agentVersion}" -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: elastic-otel-collector-agent -subjects: - - kind: ServiceAccount - name: elastic-otel-collector-agent - namespace: default ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: elastic-otel-collector-agent - namespace: default - labels: - app.kubernetes.io/name: elastic-opentelemetry-collector - app.kubernetes.io/version: "${agentVersion}" -data: - otel.yaml: | - exporters: - debug: - verbosity: basic - elasticsearch: - endpoints: - - \${env:ES_ENDPOINT} - api_key: \${env:ES_API_KEY} - logs_dynamic_index: - enabled: true - mapping: - mode: ecs - processors: - elasticinframetrics: - add_system_metrics: true - add_k8s_metrics: true - resourcedetection/eks: - detectors: [env, eks] - timeout: 15s - override: true - eks: - resource_attributes: - k8s.cluster.name: - enabled: true - resourcedetection/gcp: - detectors: [env, gcp] - timeout: 2s - override: false - resource/k8s: - attributes: - - key: service.name - from_attribute: app.label.component - action: insert - attributes/k8s_logs_dataset: - actions: - - key: data_stream.dataset - value: "kubernetes.container_logs" - action: upsert - attributes/dataset: - actions: - - key: event.dataset - from_attribute: data_stream.dataset - action: upsert - resource/cloud: - attributes: - - key: cloud.instance.id - from_attribute: host.id - action: insert - resource/process: - attributes: - - key: process.executable.name - action: delete - - key: process.executable.path - action: delete - resourcedetection/system: - detectors: ["system", "ec2"] - system: - hostname_sources: [ "os" ] - resource_attributes: - host.name: - enabled: true - host.id: - enabled: false - host.arch: - enabled: true - host.ip: - enabled: true - host.mac: - enabled: true - host.cpu.vendor.id: - enabled: true - host.cpu.family: - enabled: true - host.cpu.model.id: - enabled: true - host.cpu.model.name: - enabled: true - host.cpu.stepping: - enabled: true - host.cpu.cache.l2.size: - enabled: true - os.description: - enabled: true - os.type: - enabled: true - ec2: - resource_attributes: - host.name: - enabled: false - host.id: - enabled: true - k8sattributes: - filter: - node_from_env_var: K8S_NODE_NAME - passthrough: false - pod_association: - - sources: - - from: resource_attribute - name: k8s.pod.ip - - sources: - - from: resource_attribute - name: k8s.pod.uid - - sources: - - from: connection - extract: - metadata: - - "k8s.namespace.name" - - "k8s.deployment.name" - - "k8s.statefulset.name" - - "k8s.daemonset.name" - - "k8s.cronjob.name" - - "k8s.job.name" - - "k8s.node.name" - - "k8s.pod.name" - - "k8s.pod.uid" - - "k8s.pod.start_time" - labels: - - tag_name: app.label.component - key: app.kubernetes.io/component - from: pod - extensions: - file_storage: - directory: /var/lib/otelcol - receivers: - filelog: - retry_on_failure: - enabled: true - start_at: end - exclude: - - /var/log/pods/default_elastic-otel-collector-agent*_*/elastic-opentelemetry-collector/*.log - include: - - /var/log/pods/*/*/*.log - include_file_name: false - include_file_path: true - storage: file_storage - operators: - - id: container-parser - type: container - hostmetrics: - collection_interval: 10s - root_path: /hostfs - scrapers: - cpu: - metrics: - system.cpu.utilization: - enabled: true - system.cpu.logical.count: - enabled: true - memory: - metrics: - system.memory.utilization: - enabled: true - process: - mute_process_exe_error: true - mute_process_io_error: true - mute_process_user_error: true - metrics: - process.threads: - enabled: true - process.open_file_descriptors: - enabled: true - process.memory.utilization: - enabled: true - process.disk.operations: - enabled: true - network: - processes: - load: - disk: - filesystem: - exclude_mount_points: - mount_points: - - /dev/* - - /proc/* - - /sys/* - - /run/k3s/containerd/* - - /var/lib/docker/* - - /var/lib/kubelet/* - - /snap/* - match_type: regexp - exclude_fs_types: - fs_types: - - autofs - - binfmt_misc - - bpf - - cgroup2 - - configfs - - debugfs - - devpts - - devtmpfs - - fusectl - - hugetlbfs - - iso9660 - - mqueue - - nsfs - - overlay - - proc - - procfs - - pstore - - rpc_pipefs - - securityfs - - selinuxfs - - squashfs - - sysfs - - tracefs - match_type: strict - kubeletstats: - auth_type: serviceAccount - collection_interval: 20s - endpoint: \${env:K8S_NODE_NAME}:10250 - node: '\${env:K8S_NODE_NAME}' - # Required to work for all CSPs without an issue - insecure_skip_verify: true - k8s_api_config: - auth_type: serviceAccount - metrics: - k8s.pod.cpu.node.utilization: - enabled: true - k8s.container.cpu_limit_utilization: - enabled: true - k8s.pod.cpu_limit_utilization: - enabled: true - k8s.container.cpu_request_utilization: - enabled: true - k8s.container.memory_limit_utilization: - enabled: true - k8s.pod.memory_limit_utilization: - enabled: true - k8s.container.memory_request_utilization: - enabled: true - k8s.node.uptime: - enabled: true - k8s.node.cpu.usage: - enabled: true - k8s.pod.cpu.usage: - enabled: true - extra_metadata_labels: - - container.id - - service: - extensions: [file_storage] - pipelines: - logs: - exporters: - - elasticsearch - - debug - processors: - - k8sattributes - - resourcedetection/system - - resourcedetection/eks - - resourcedetection/gcp - - resource/k8s - - resource/cloud - - attributes/k8s_logs_dataset - receivers: - - filelog - metrics: - exporters: - - debug - - elasticsearch - processors: - - k8sattributes - - elasticinframetrics - - resourcedetection/system - - resourcedetection/eks - - resourcedetection/gcp - - resource/k8s - - resource/cloud - - attributes/dataset - - resource/process - receivers: - - kubeletstats - - hostmetrics ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: elastic-otel-collector-agent - namespace: default - labels: - app.kubernetes.io/name: elastic-opentelemetry-collector - app.kubernetes.io/version: "${agentVersion}" -spec: - selector: - matchLabels: - app.kubernetes.io/name: elastic-opentelemetry-collector - app.kubernetes.io/version: "${agentVersion}" - template: - metadata: - labels: - app.kubernetes.io/name: elastic-opentelemetry-collector - app.kubernetes.io/version: "${agentVersion}" - spec: - serviceAccountName: elastic-otel-collector-agent - securityContext: - runAsUser: 0 - runAsGroup: 0 - hostNetwork: true - dnsPolicy: ClusterFirstWithHostNet - containers: - - name: elastic-opentelemetry-collector - command: [/usr/share/elastic-agent/elastic-agent] - args: ["otel", "-c", "/etc/elastic-agent/otel.yaml"] - image: docker.elastic.co/beats/elastic-agent:${agentVersion} - imagePullPolicy: IfNotPresent - env: - - name: MY_POD_IP - valueFrom: - fieldRef: - apiVersion: v1 - fieldPath: status.podIP - - name: K8S_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: ES_ENDPOINT - valueFrom: - secretKeyRef: - key: es_endpoint - name: elastic-secret-otel - - name: ES_API_KEY - valueFrom: - secretKeyRef: - key: es_api_key - name: elastic-secret-otel - volumeMounts: - - mountPath: /etc/elastic-agent/otel.yaml - name: opentelemetry-collector-configmap - readOnly: true - subPath: otel.yaml - - name: varlogpods - mountPath: /var/log/pods - readOnly: true - - name: varlibdockercontainers - mountPath: /var/lib/docker/containers - readOnly: true - - name: varlibotelcol - mountPath: /var/lib/otelcol - - name: hostfs - mountPath: /hostfs - readOnly: true - mountPropagation: HostToContainer - - volumes: - - name: opentelemetry-collector-configmap - configMap: - name: elastic-otel-collector-agent - defaultMode: 0640 - - name: varlogpods - hostPath: - path: /var/log/pods - - name: varlibdockercontainers - hostPath: - path: /var/lib/docker/containers - - name: varlibotelcol - hostPath: - path: /var/lib/otelcol - type: DirectoryOrCreate - - name: hostfs - hostPath: - path: /`, - type: 'download', - fileName: 'otel-collector-k8s.yml', - }, { id: 'linux', name: 'Linux', @@ -613,39 +161,20 @@ rm ./otel.yml && cp ./otel_samples/platformlogs_hostmetrics.yml ./otel.yml && mk
- {selectedContent.type === 'download' ? ( - - {i18n.translate( - 'xpack.observability_onboarding.installOtelCollector.configStep.downloadConfigButton', - { defaultMessage: 'Download manifest' } - )} - - ) : ( - - {(copy) => ( - - {i18n.translate( - 'xpack.observability_onboarding.installOtelCollector.configStep.copyCommand', - { defaultMessage: 'Copy to clipboard' } - )} - - )} - - )} + + {(copy) => ( + + {i18n.translate( + 'xpack.observability_onboarding.installOtelCollector.configStep.copyCommand', + { defaultMessage: 'Copy to clipboard' } + )} + + )} + @@ -673,35 +202,28 @@ rm ./otel.yml && cp ./otel_samples/platformlogs_hostmetrics.yml ./otel.yml && mk } )}

- {selectedTab !== 'kubernetes' && ( -

- {i18n.translate( - 'xpack.observability_onboarding.otelLogsPanel.historicalDataDescription2', - { - defaultMessage: - 'The default log path is /var/log/*. You can change this path in the otel.yml file if needed.', - } - )} -

- )} +

+ {i18n.translate( + 'xpack.observability_onboarding.otelLogsPanel.historicalDataDescription2', + { + defaultMessage: + 'The default log path is /var/log/*. You can change this path in the otel.yml file if needed.', + } + )} +

- {selectedContent.prompt} - {selectedContent.start && ( - <> - -

- {i18n.translate( - 'xpack.observability_onboarding.otelLogsPanel.p.startTheCollectorLabel', - { - defaultMessage: 'Run the following command to start the collector', - } - )} -

-
- - - )} + +

+ {i18n.translate( + 'xpack.observability_onboarding.otelLogsPanel.p.startTheCollectorLabel', + { + defaultMessage: 'Run the following command to start the collector', + } + )} +

+
+ ), }, diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/shared/logo_icon.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/shared/logo_icon.tsx index bd95473c15617..9efba65d1c299 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/shared/logo_icon.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/shared/logo_icon.tsx @@ -27,6 +27,7 @@ export type SupportedLogo = | 'mysql' | 'postgresql' | 'redis' + | 'ruby' | 'haproxy' | 'rabbitmq' | 'kafka' @@ -54,6 +55,7 @@ export function isSupportedLogo(logo: string): logo is SupportedLogo { 'mysql', 'postgresql', 'redis', + 'ruby', 'haproxy', 'rabbitmq', 'kafka', diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/assets/java.svg b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/java.svg index 943e009ec8dfe..73e5416d10c73 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/assets/java.svg +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/java.svg @@ -1,3 +1,7 @@ - - + + + + + + diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/assets/ruby.svg b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/ruby.svg new file mode 100644 index 0000000000000..22398b4d03641 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/assets/ruby.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_install_api_key.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_install_api_key.ts index eddc5e10b5c65..3a5680b499055 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_install_api_key.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_install_api_key.ts @@ -21,10 +21,7 @@ export function createInstallApiKey(name: string): CreateAPIKeyParams { }, kibana_role_descriptors: { can_install_integrations: { - elasticsearch: { - cluster: [], - indices: [], - }, + elasticsearch: {}, kibana: [ { feature: { diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_shipper_api_key.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_shipper_api_key.ts index 942ebdbbd07cd..bdfdd202a962e 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_shipper_api_key.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/create_shipper_api_key.ts @@ -6,9 +6,9 @@ */ import { ElasticsearchClient } from '@kbn/core/server'; -import { cluster, indices } from './monitoring_config'; +import { MONITOR_CLUSTER, INDEX_LOGS_AND_METRICS, WRITE_APM_EVENTS } from './privileges'; -export function createShipperApiKey(esClient: ElasticsearchClient, name: string) { +export function createShipperApiKey(esClient: ElasticsearchClient, name: string, withAPM = false) { // Based on https://www.elastic.co/guide/en/fleet/master/grant-access-to-elasticsearch.html#create-api-key-standalone-agent return esClient.security.createApiKey({ body: { @@ -19,8 +19,9 @@ export function createShipperApiKey(esClient: ElasticsearchClient, name: string) }, role_descriptors: { standalone_agent: { - cluster, - indices, + cluster: [MONITOR_CLUSTER], + indices: [INDEX_LOGS_AND_METRICS], + applications: withAPM ? [WRITE_APM_EVENTS] : undefined, }, }, }, diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/has_log_monitoring_privileges.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/has_log_monitoring_privileges.ts index a7aec8eefa293..0593a7f761e1e 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/has_log_monitoring_privileges.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/has_log_monitoring_privileges.ts @@ -6,13 +6,14 @@ */ import { ElasticsearchClient } from '@kbn/core/server'; -import { cluster, indices } from './monitoring_config'; +import { MONITOR_CLUSTER, INDEX_LOGS_AND_METRICS, WRITE_APM_EVENTS } from './privileges'; -export async function hasLogMonitoringPrivileges(esClient: ElasticsearchClient) { +export async function hasLogMonitoringPrivileges(esClient: ElasticsearchClient, withAPM = false) { const res = await esClient.security.hasPrivileges({ body: { - index: indices, - cluster: [...cluster, 'manage_own_api_key'], + cluster: [MONITOR_CLUSTER, 'manage_own_api_key'], + index: [INDEX_LOGS_AND_METRICS], + application: withAPM ? [WRITE_APM_EVENTS] : undefined, }, }); diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/privileges.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/privileges.ts new file mode 100644 index 0000000000000..7c3b5999842bd --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/lib/api_key/privileges.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { estypes } from '@elastic/elasticsearch'; + +/** + * Grants all cluster read-only operations, like cluster health and state, hot threads, node info, node and cluster stats, and pending cluster tasks. + */ +export const MONITOR_CLUSTER: estypes.SecurityClusterPrivilege = 'monitor'; + +// https://www.elastic.co/guide/en/fleet/master/grant-access-to-elasticsearch.html#create-api-key-standalone-agent +export const INDEX_LOGS_AND_METRICS: estypes.SecurityIndicesPrivileges = { + names: ['logs-*-*', 'metrics-*-*'], + privileges: ['auto_configure', 'create_doc'], +}; + +// https://www.elastic.co/guide/en/observability/master/apm-api-key.html#apm-create-api-key-workflow-es +export const WRITE_APM_EVENTS: estypes.SecurityApplicationPrivileges = { + application: 'apm', + privileges: ['event:write', 'config_agent:read'], + resources: ['*'], +}; diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts index 33a501bd184b9..691c28f5a14e6 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/kubernetes/route.ts @@ -45,7 +45,7 @@ const createKubernetesOnboardingFlowRoute = createObservabilityOnboardingServerR elasticsearch: { client }, } = await context.core; - const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser); + const hasPrivileges = await hasLogMonitoringPrivileges(client.asCurrentUser, true); if (!hasPrivileges) { throw Boom.forbidden( @@ -57,7 +57,7 @@ const createKubernetesOnboardingFlowRoute = createObservabilityOnboardingServerR const packageClient = fleetPluginStart.packageService.asScoped(request); const [{ encoded: apiKeyEncoded }, elasticAgentVersion] = await Promise.all([ - createShipperApiKey(client.asCurrentUser, 'kubernetes_onboarding'), + createShipperApiKey(client.asCurrentUser, `${params.body.pkgName}_onboarding`, true), getAgentVersion(fleetPluginStart, kibanaVersion), // System package is always required packageClient.ensureInstalledPackage({ pkgName: 'system' }), diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/test_helpers/create_observability_onboarding_users/authentication.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/test_helpers/create_observability_onboarding_users/authentication.ts index 340f0cb615651..eafcc5a2e92c6 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/test_helpers/create_observability_onboarding_users/authentication.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/test_helpers/create_observability_onboarding_users/authentication.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { cluster, indices } from '../../lib/api_key/monitoring_config'; +import { MONITOR_CLUSTER, INDEX_LOGS_AND_METRICS } from '../../lib/api_key/privileges'; export enum ObservabilityOnboardingUsername { noAccessUser = 'no_access_user', @@ -21,8 +21,8 @@ export enum ObservabilityOnboardingCustomRolename { export const customRoles = { [ObservabilityOnboardingCustomRolename.logMonitoringUser]: { elasticsearch: { - cluster: [...cluster, 'manage_own_api_key'], - indices, + cluster: [MONITOR_CLUSTER, 'manage_own_api_key'], + indices: [INDEX_LOGS_AND_METRICS], }, }, }; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts index 6e24b902995f4..374c6ff492e8d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule.ts @@ -21,11 +21,8 @@ import { RequiredFieldArray, RiskScore, RiskScoreMapping, - RuleAuthorArray, RuleDescription, - RuleExceptionList, RuleFalsePositiveArray, - RuleLicense, RuleName, RuleReferenceArray, RuleSignatureId, @@ -82,12 +79,9 @@ export const DiffableCommonFields = z.object({ setup: SetupGuide, related_integrations: RelatedIntegrationArray, required_fields: RequiredFieldArray, - author: RuleAuthorArray, - license: RuleLicense, // Other domain fields rule_schedule: RuleSchedule, // NOTE: new field - exceptions_list: z.array(RuleExceptionList), max_signals: MaxSignals, // Optional fields diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts index 784f75d09bd7a..0021aece67455 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -40,7 +40,7 @@ export const FIELDS_TO_UPGRADE_TO_CURRENT_VERSION = [ 'items_per_search', ] as const; -export const NON_UPGRADEABLE_DIFFABLE_FIELDS = [ +export const FIELDS_TO_UPGRADE_TO_TARGET_VERSION = [ 'type', 'rule_id', 'version', @@ -48,6 +48,10 @@ export const NON_UPGRADEABLE_DIFFABLE_FIELDS = [ 'license', ] as const; +// Fields which are part of DiffableRule but are not upgradeable +// and need to be omittted from the DiffableUpgradableFields +export const NON_UPGRADEABLE_DIFFABLE_FIELDS = ['type', 'rule_id', 'version'] as const; + type NON_UPGRADEABLE_DIFFABLE_FIELDS_TO_OMIT_TYPE = { readonly [key in (typeof NON_UPGRADEABLE_DIFFABLE_FIELDS)[number]]: true; }; diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts index 41615f24d011c..151fb05f41856 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts @@ -23,6 +23,7 @@ import { Note } from '../model/components.gen'; */ export type AssociatedFilterType = z.infer; export const AssociatedFilterType = z.enum([ + 'all', 'document_only', 'saved_object_only', 'document_and_saved_object', diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml index 4873d0e90ef2a..e635018c293cf 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml @@ -75,10 +75,11 @@ components: AssociatedFilterType: type: string enum: - - document_only - - saved_object_only - - document_and_saved_object - - orphan + - all + - document_only + - saved_object_only + - document_and_saved_object + - orphan description: Filter notes based on their association with a document or saved object. DocumentIds: oneOf: diff --git a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts index 45b4612e83c8e..0f70a86c54e29 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules/diff/convert_rule_to_diffable.ts @@ -133,12 +133,9 @@ const extractDiffableCommonFields = ( setup: rule.setup ?? '', related_integrations: rule.related_integrations ?? [], required_fields: addEcsToRequiredFields(rule.required_fields), - author: rule.author ?? [], - license: rule.license ?? '', // Other domain fields rule_schedule: extractRuleSchedule(rule), - exceptions_list: rule.exceptions_list ?? [], max_signals: rule.max_signals ?? DEFAULT_MAX_SIGNALS, // --------------------- OPTIONAL FIELDS diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index ae0025305a7e7..070d7be235ef5 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -915,6 +915,7 @@ components: AssociatedFilterType: description: Filter notes based on their association with a document or saved object. enum: + - all - document_only - saved_object_only - document_and_saved_object diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index bf59a31c8de9e..9f5882f0f4072 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -915,6 +915,7 @@ components: AssociatedFilterType: description: Filter notes based on their association with a document or saved object. enum: + - all - document_only - saved_object_only - document_and_saved_object diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx index f496ecc89b90b..e1e3bac6d9c18 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigate_in_timeline_button.tsx @@ -8,7 +8,7 @@ import type { FC, PropsWithChildren } from 'react'; import React, { useCallback } from 'react'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; -import type { IconType } from '@elastic/eui'; +import type { IconType, EuiButtonEmptyProps } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; import { useDispatch, useSelector } from 'react-redux'; @@ -34,6 +34,7 @@ export interface InvestigateInTimelineButtonProps { isDisabled?: boolean; iconType?: IconType; children?: React.ReactNode; + flush?: EuiButtonEmptyProps['flush']; } export const InvestigateInTimelineButton: FC< @@ -46,6 +47,7 @@ export const InvestigateInTimelineButton: FC< timeRange, keepDataView, iconType, + flush, ...rest }) => { const dispatch = useDispatch(); @@ -118,7 +120,7 @@ export const InvestigateInTimelineButton: FC< diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts index 2f5065eb113be..eabd2050e7ba2 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/hooks/use_esql_index.test.ts @@ -27,8 +27,16 @@ describe('useEsqlIndex', () => { expect(result.current).toEqual([]); }); + it('returns indices which appear in source before syntax error', async () => { + const typeErrorCausingQuery = 'from auditbeat* [, auditbeat2*'; + + const { result } = renderHook(() => useEsqlIndex(typeErrorCausingQuery, 'esql')); + + expect(result.current).toEqual(['auditbeat*']); + }); + it('should return empty array if invalid query is causing a TypeError in ES|QL parser', async () => { - const typeErrorCausingQuery = 'from auditbeat* []'; + const typeErrorCausingQuery = 'from []'; const { result } = renderHook(() => useEsqlIndex(typeErrorCausingQuery, 'esql')); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts index 5d42c8d73fd84..04660191c9cbf 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/constants.ts @@ -14,7 +14,6 @@ export const ABOUT_UPGRADE_FIELD_ORDER: Array = [ 'version', 'name', 'description', - 'author', 'building_block', 'investigation_fields', 'severity', @@ -23,7 +22,6 @@ export const ABOUT_UPGRADE_FIELD_ORDER: Array = [ 'risk_score_mapping', 'references', 'false_positives', - 'license', 'rule_name_override', 'threat', 'threat_indicator_path', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/constants.ts deleted file mode 100644 index ad348c85148b2..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/components/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { DiffableAllFields } from '../../../../../../../common/api/detection_engine'; - -type NonEditableFields = Readonly>; - -/* These fields are not visible in the comparison UI and are not editable */ -export const HIDDEN_FIELDS: NonEditableFields = new Set([ - 'alert_suppression', - 'author', - 'rule_id', - 'license', - 'version', -]); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx index 9471a17b216b3..bc4f1928ef9ba 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx @@ -19,11 +19,9 @@ import { NameReadOnly } from './fields/name/name'; import { TagsReadOnly } from './fields/tags/tags'; import { DescriptionReadOnly } from './fields/description/description'; import { assertUnreachable } from '../../../../../../../common/utility_types'; -import { AuthorReadOnly } from './fields/author/author'; import { BuildingBlockReadOnly } from './fields/building_block/building_block'; import { InvestigationFieldsReadOnly } from './fields/investigation_fields/investigation_fields'; import { FalsePositivesReadOnly } from './fields/false_positives/false_positives'; -import { LicenseReadOnly } from './fields/license/license'; import { MaxSignalsReadOnly } from './fields/max_signals/max_signals'; import { NoteReadOnly } from './fields/note/note'; import { RuleScheduleReadOnly } from './fields/rule_schedule/rule_schedule'; @@ -46,23 +44,16 @@ export function CommonRuleFieldReadOnly({ finalDiffableRule, }: CommonRuleFieldReadOnlyProps) { switch (fieldName) { - case 'author': - return ; case 'building_block': return ; case 'description': return ; - case 'exceptions_list': - /* Exceptions are not used in prebuilt rules */ - return null; case 'investigation_fields': return ( ); case 'false_positives': return ; - case 'license': - return ; case 'max_signals': return ; case 'name': diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.stories.tsx deleted file mode 100644 index 97526ec0290b9..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.stories.tsx +++ /dev/null @@ -1,39 +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 from 'react'; -import type { Story } from '@storybook/react'; -import { AuthorReadOnly } from './author'; -import { FieldReadOnly } from '../../field_readonly'; -import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; -import { mockCustomQueryRule } from '../../storybook/mocks'; -import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers'; - -export default { - component: AuthorReadOnly, - title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/author', -}; - -interface TemplateProps { - finalDiffableRule: DiffableRule; -} - -const Template: Story = (args) => { - return ( - - - - ); -}; - -export const Default = Template.bind({}); - -Default.args = { - finalDiffableRule: mockCustomQueryRule({ - author: ['Elastic', 'John Doe'], - }), -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.tsx deleted file mode 100644 index c284275bd4db4..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/author/author.tsx +++ /dev/null @@ -1,29 +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 from 'react'; -import { EuiDescriptionList } from '@elastic/eui'; -import * as ruleDetailsI18n from '../../../../translations'; -import type { RuleAuthorArray } from '../../../../../../../../../common/api/detection_engine'; -import { Author } from '../../../../rule_about_section'; - -interface AuthorReadOnlyProps { - author: RuleAuthorArray; -} - -export function AuthorReadOnly({ author }: AuthorReadOnlyProps) { - return ( - , - }, - ]} - /> - ); -} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.stories.tsx deleted file mode 100644 index 666f4b6507798..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.stories.tsx +++ /dev/null @@ -1,39 +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 from 'react'; -import type { Story } from '@storybook/react'; -import { LicenseReadOnly } from './license'; -import { FieldReadOnly } from '../../field_readonly'; -import type { DiffableRule } from '../../../../../../../../../common/api/detection_engine'; -import { mockCustomQueryRule } from '../../storybook/mocks'; -import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers'; - -export default { - component: LicenseReadOnly, - title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/license', -}; - -interface TemplateProps { - finalDiffableRule: DiffableRule; -} - -const Template: Story = (args) => { - return ( - - - - ); -}; - -export const Default = Template.bind({}); - -Default.args = { - finalDiffableRule: mockCustomQueryRule({ - license: 'Elastic License 2.0', - }), -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.tsx deleted file mode 100644 index 18032f66ab81d..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/license/license.tsx +++ /dev/null @@ -1,29 +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 from 'react'; -import { EuiDescriptionList } from '@elastic/eui'; -import * as ruleDetailsI18n from '../../../../translations'; -import type { RuleLicense } from '../../../../../../../../../common/api/detection_engine'; -import { License } from '../../../../rule_about_section'; - -interface LicenseReadOnlyProps { - license: RuleLicense; -} - -export function LicenseReadOnly({ license }: LicenseReadOnlyProps) { - return ( - , - }, - ]} - /> - ); -} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts index e940f1ba52a40..18973df5ca545 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/storybook/mocks.ts @@ -137,14 +137,10 @@ const commonDiffableRuleFields: DiffableCommonFields = { setup: '', related_integrations: [], required_fields: [], - author: [], - license: '', - rule_schedule: { interval: '5m', lookback: '360s', }, - exceptions_list: [], max_signals: DEFAULT_MAX_SIGNALS, }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/fields.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/fields.ts index c384b48a79d4f..f3b98ba3d0dd9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/fields.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/fields.ts @@ -4,18 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { DiffableCommonFields } from '../../../../../common/api/detection_engine'; -import type { - DiffableCustomQueryFields, - DiffableEqlFields, - DiffableEsqlFields, - DiffableMachineLearningFields, - DiffableNewTermsFields, - DiffableSavedQueryFields, - DiffableThreatMatchFields, - DiffableThresholdFields, - RuleFieldsDiff, +import { + DiffableCommonFields, + NON_UPGRADEABLE_DIFFABLE_FIELDS, + type DiffableCustomQueryFields, + type DiffableEqlFields, + type DiffableEsqlFields, + type DiffableMachineLearningFields, + type DiffableNewTermsFields, + type DiffableSavedQueryFields, + type DiffableThreatMatchFields, + type DiffableThresholdFields, + type RuleFieldsDiff, } from '../../../../../common/api/detection_engine'; export type NonUpgradeableDiffableFields = (typeof NON_UPGRADEABLE_DIFFABLE_FIELDS)[number]; @@ -61,14 +61,6 @@ export type UpgradeableNewTermsFields = Exclude< NonUpgradeableDiffableFields >; -export const NON_UPGRADEABLE_DIFFABLE_FIELDS = [ - 'author', - 'license', - 'rule_id', - 'type', - 'version', -] as const; - export const COMMON_FIELD_NAMES = DiffableCommonFields.keyof().options; export function isCommonFieldName(fieldName: string): fieldName is keyof DiffableCommonFields { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx index f0d16a418f2b2..5e4650179291d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.test.tsx @@ -48,6 +48,7 @@ describe('AlertCountInsight', () => { const { getByTestId } = renderAlertCountInsight(); expect(getByTestId(testId)).toBeInTheDocument(); expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('177'); }); it('renders loading spinner if data is being fetched', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx index 566b77b5739a9..08325584bd8cb 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/alert_count_insight.tsx @@ -16,8 +16,12 @@ import { getIsAlertsBySeverityData, getSeverityColor, } from '../../../../detections/components/alerts_kpis/severity_level_panel/helpers'; +import { FormattedCount } from '../../../../common/components/formatted_number'; +import { InvestigateInTimelineButton } from '../../../../common/components/event_details/investigate_in_timeline_button'; +import { getDataProvider } from '../../../../common/components/event_details/use_action_cell_data_provider'; const ENTITY_ALERT_COUNT_ID = 'entity-alert-count'; +const SEVERITIES = ['unknown', 'low', 'medium', 'high', 'critical']; interface AlertCountInsightProps { /** @@ -39,7 +43,7 @@ interface AlertCountInsightProps { } /* - * Displays a distribution bar with the count of critical alerts for a given entity + * Displays a distribution bar with the total alert count for a given entity */ export const AlertCountInsight: React.FC = ({ name, @@ -56,22 +60,27 @@ export const AlertCountInsight: React.FC = ({ uniqueQueryId, signalIndexName: null, }); + const dataProviders = useMemo( + () => [getDataProvider(fieldName, `timeline-indicator-${fieldName}-${name}`, name)], + [fieldName, name] + ); const data = useMemo(() => (getIsAlertsBySeverityData(items) ? items : []), [items]); - const alertStats = useMemo(() => { - return data.map((item) => ({ - key: item.key, - count: item.value, - color: getSeverityColor(item.key), - })); - }, [data]); - - const count = useMemo( - () => data.filter((item) => item.key === 'critical')[0]?.value ?? 0, + const alertStats = useMemo( + () => + data + .map((item) => ({ + key: item.key, + count: item.value, + color: getSeverityColor(item.key), + })) + .sort((a, b) => SEVERITIES.indexOf(a.key) - SEVERITIES.indexOf(b.key)), [data] ); + const totalAlertCount = useMemo(() => data.reduce((acc, item) => acc + item.value, 0), [data]); + if (!isLoading && items.length === 0) return null; return ( @@ -87,7 +96,17 @@ export const AlertCountInsight: React.FC = ({ /> } stats={alertStats} - count={count} + count={ +
+ + + +
+ } direction={direction} data-test-subj={`${dataTestSubj}-distribution-bar`} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx index a775da8a7f73a..405c0528a9b2c 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.test.tsx @@ -11,7 +11,7 @@ import { InsightDistributionBar } from './insight_distribution_bar'; import { TestProviders } from '../../../../common/mock'; const title = 'test title'; -const count = 10; +const count =
{'100'}
; const testId = 'test-id'; const stats = [ { @@ -35,7 +35,7 @@ describe('', () => { ); expect(getByTestId(testId)).toBeInTheDocument(); expect(getByText(title)).toBeInTheDocument(); - expect(getByTestId(`${testId}-badge`)).toHaveTextContent(`${count}`); + expect(getByTestId('test-count')).toBeInTheDocument(); expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx index 006ec8c5dad4f..083738e6766bc 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/insight_distribution_bar.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { css } from '@emotion/css'; import { EuiFlexGroup, @@ -17,7 +17,6 @@ import { type EuiFlexGroupProps, } from '@elastic/eui'; import { DistributionBar } from '@kbn/security-solution-distribution-bar'; -import { FormattedCount } from '../../../../common/components/formatted_number'; export interface InsightDistributionBarProps { /** @@ -31,7 +30,7 @@ export interface InsightDistributionBarProps { /** * Count to be displayed in the badge */ - count: number; + count: React.ReactNode; /** * Flex direction of the component */ @@ -53,34 +52,53 @@ export const InsightDistributionBar: React.FC = ({ const { euiTheme } = useEuiTheme(); const xsFontSize = useEuiFontSize('xs').fontSize; + const barComponent = useMemo( + () => ( + + + + + + {count} + + + ), + [stats, count, dataTestSubj] + ); + return ( - - + + {title} - - - - - - - - - - - - + {direction === 'column' ? ( + + {barComponent} + + ) : ( + {barComponent} + )} ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx index 296a61f444a17..8976a01eedbc4 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.test.tsx @@ -10,34 +10,87 @@ import { render } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { MisconfigurationsInsight } from './misconfiguration_insight'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; +import { DocumentDetailsContext } from '../context'; +import { mockFlyoutApi } from '../mocks/mock_flyout_context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { mockContextValue } from '../mocks/mock_context'; +import { HostPreviewPanelKey } from '../../../entity_details/host_right'; +import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; +import { UserPreviewPanelKey } from '../../../entity_details/user_right'; +import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview'; +jest.mock('@kbn/expandable-flyout'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'); -const fieldName = 'host.name'; -const name = 'test host'; +const hostName = 'test host'; +const userName = 'test user'; const testId = 'test'; -const renderMisconfigurationsInsight = () => { +const renderMisconfigurationsInsight = (fieldName: 'host.name' | 'user.name', value: string) => { return render( - + + + ); }; describe('MisconfigurationsInsight', () => { + beforeEach(() => { + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + it('renders', () => { (useMisconfigurationPreview as jest.Mock).mockReturnValue({ data: { count: { passed: 1, failed: 2 } }, }); - const { getByTestId } = renderMisconfigurationsInsight(); + const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName); expect(getByTestId(testId)).toBeInTheDocument(); expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); }); it('renders null if no misconfiguration data found', () => { (useMisconfigurationPreview as jest.Mock).mockReturnValue({}); - const { container } = renderMisconfigurationsInsight(); + const { container } = renderMisconfigurationsInsight('host.name', hostName); expect(container).toBeEmptyDOMElement(); }); + + describe('should open entity flyout when clicking on badge', () => { + it('should open host entity flyout when clicking on host badge', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 1, failed: 2 } }, + }); + const { getByTestId } = renderMisconfigurationsInsight('host.name', hostName); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('3'); + + getByTestId(`${testId}-count`).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: HostPreviewPanelKey, + params: { + hostName, + banner: HOST_PREVIEW_BANNER, + scopeId: mockContextValue.scopeId, + }, + }); + }); + + it('should open user entity flyout when clicking on user badge', () => { + (useMisconfigurationPreview as jest.Mock).mockReturnValue({ + data: { count: { passed: 2, failed: 3 } }, + }); + const { getByTestId } = renderMisconfigurationsInsight('user.name', userName); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('5'); + + getByTestId(`${testId}-count`).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: UserPreviewPanelKey, + params: { + userName, + banner: USER_PREVIEW_BANNER, + scopeId: mockContextValue.scopeId, + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx index 552a242c84893..961fa1d5f3a45 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/misconfiguration_insight.tsx @@ -6,12 +6,16 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/css'; import { useMisconfigurationPreview } from '@kbn/cloud-security-posture/src/hooks/use_misconfiguration_preview'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { InsightDistributionBar } from './insight_distribution_bar'; import { getFindingsStats } from '../../../../cloud_security_posture/components/misconfiguration/misconfiguration_preview'; +import { FormattedCount } from '../../../../common/components/formatted_number'; +import { PreviewLink } from '../../../shared/components/preview_link'; +import { useDocumentDetailsContext } from '../context'; interface MisconfigurationsInsightProps { /** @@ -33,7 +37,7 @@ interface MisconfigurationsInsightProps { } /* - * Displays a distribution bar with the count of failed misconfigurations for a given entity + * Displays a distribution bar with the count of total misconfigurations for a given entity */ export const MisconfigurationsInsight: React.FC = ({ name, @@ -41,6 +45,8 @@ export const MisconfigurationsInsight: React.FC = direction, 'data-test-subj': dataTestSubj, }) => { + const { scopeId, isPreview } = useDocumentDetailsContext(); + const { euiTheme } = useEuiTheme(); const { data } = useMisconfigurationPreview({ query: buildEntityFlyoutPreviewQuery(fieldName, name), sort: [], @@ -50,13 +56,39 @@ export const MisconfigurationsInsight: React.FC = const passedFindings = data?.count.passed || 0; const failedFindings = data?.count.failed || 0; - const hasMisconfigurationFindings = passedFindings > 0 || failedFindings > 0; + const totalFindings = useMemo( + () => passedFindings + failedFindings, + [passedFindings, failedFindings] + ); + const hasMisconfigurationFindings = totalFindings > 0; const misconfigurationsStats = useMemo( () => getFindingsStats(passedFindings, failedFindings), [passedFindings, failedFindings] ); + const count = useMemo( + () => ( +
+ + + +
+ ), + [totalFindings, fieldName, name, scopeId, isPreview, dataTestSubj, euiTheme.size] + ); + if (!hasMisconfigurationFindings) return null; return ( @@ -69,7 +101,7 @@ export const MisconfigurationsInsight: React.FC = /> } stats={misconfigurationsStats} - count={failedFindings} + count={count} direction={direction} data-test-subj={`${dataTestSubj}-distribution-bar`} /> diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx index 77c6737266b89..cfac8703fbc89 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.test.tsx @@ -10,7 +10,14 @@ import { render } from '@testing-library/react'; import React from 'react'; import { VulnerabilitiesInsight } from './vulnerabilities_insight'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; +import { DocumentDetailsContext } from '../context'; +import { mockFlyoutApi } from '../mocks/mock_flyout_context'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { mockContextValue } from '../mocks/mock_context'; +import { HostPreviewPanelKey } from '../../../entity_details/host_right'; +import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview'; +jest.mock('@kbn/expandable-flyout'); jest.mock('@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'); const hostName = 'test host'; @@ -19,15 +26,21 @@ const testId = 'test'; const renderVulnerabilitiesInsight = () => { return render( - + + + ); }; describe('VulnerabilitiesInsight', () => { + beforeEach(() => { + jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi); + }); + it('renders', () => { (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ - data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, UNKNOWN: 0 } }, + data: { count: { CRITICAL: 0, HIGH: 1, MEDIUM: 1, LOW: 0, NONE: 0 } }, }); const { getByTestId } = renderVulnerabilitiesInsight(); @@ -35,6 +48,24 @@ describe('VulnerabilitiesInsight', () => { expect(getByTestId(`${testId}-distribution-bar`)).toBeInTheDocument(); }); + it('opens host preview when click on count badge', () => { + (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({ + data: { count: { CRITICAL: 1, HIGH: 2, MEDIUM: 1, LOW: 2, NONE: 2 } }, + }); + const { getByTestId } = renderVulnerabilitiesInsight(); + expect(getByTestId(`${testId}-count`)).toHaveTextContent('8'); + + getByTestId(`${testId}-count`).click(); + expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({ + id: HostPreviewPanelKey, + params: { + hostName, + banner: HOST_PREVIEW_BANNER, + scopeId: mockContextValue.scopeId, + }, + }); + }); + it('renders null when data is not available', () => { (useVulnerabilitiesPreview as jest.Mock).mockReturnValue({}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx index 4c581b6db57d0..c675c0a0e079b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/components/vulnerabilities_insight.tsx @@ -6,12 +6,16 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexItem, type EuiFlexGroupProps } from '@elastic/eui'; +import { EuiFlexItem, type EuiFlexGroupProps, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/css'; import { useVulnerabilitiesPreview } from '@kbn/cloud-security-posture/src/hooks/use_vulnerabilities_preview'; import { buildEntityFlyoutPreviewQuery } from '@kbn/cloud-security-posture-common'; import { getVulnerabilityStats, hasVulnerabilitiesData } from '@kbn/cloud-security-posture'; import { InsightDistributionBar } from './insight_distribution_bar'; +import { FormattedCount } from '../../../../common/components/formatted_number'; +import { PreviewLink } from '../../../shared/components/preview_link'; +import { useDocumentDetailsContext } from '../context'; interface VulnerabilitiesInsightProps { /** @@ -29,13 +33,15 @@ interface VulnerabilitiesInsightProps { } /* - * Displays a distribution bar with the count of critical vulnerabilities for a given host + * Displays a distribution bar and the total vulnerabilities count for a given host */ export const VulnerabilitiesInsight: React.FC = ({ hostName, direction, 'data-test-subj': dataTestSubj, }) => { + const { scopeId, isPreview } = useDocumentDetailsContext(); + const { euiTheme } = useEuiTheme(); const { data } = useVulnerabilitiesPreview({ query: buildEntityFlyoutPreviewQuery('host.name', hostName), sort: [], @@ -44,6 +50,11 @@ export const VulnerabilitiesInsight: React.FC = ({ }); const { CRITICAL = 0, HIGH = 0, MEDIUM = 0, LOW = 0, NONE = 0 } = data?.count || {}; + const totalVulnerabilities = useMemo( + () => CRITICAL + HIGH + MEDIUM + LOW + NONE, + [CRITICAL, HIGH, MEDIUM, LOW, NONE] + ); + const hasVulnerabilitiesFindings = useMemo( () => hasVulnerabilitiesData({ @@ -68,6 +79,28 @@ export const VulnerabilitiesInsight: React.FC = ({ [CRITICAL, HIGH, MEDIUM, LOW, NONE] ); + const count = useMemo( + () => ( +
+ + + +
+ ), + [totalVulnerabilities, hostName, scopeId, isPreview, dataTestSubj, euiTheme.size] + ); + if (!hasVulnerabilitiesFindings) return null; return ( @@ -80,7 +113,7 @@ export const VulnerabilitiesInsight: React.FC = ({ /> } stats={vulnerabilitiesStats} - count={CRITICAL} + count={count} direction={direction} data-test-subj={`${dataTestSubj}-distribution-bar`} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index d948cb5711f38..964936b310a41 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -2014,7 +2014,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.events.aggregate_process', { defaultMessage: - 'Reduce event volume by merging related process events into fewer aggregate events. Default is true.', + 'Reduce event volume by merging related process events into fewer aggregate events. Default is false.', } ), }, @@ -2025,7 +2025,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.events.aggregate_process', { defaultMessage: - 'Reduce event volume by merging related process events into fewer aggregate events. Default is true.', + 'Reduce event volume by merging related process events into fewer aggregate events. Default is false.', } ), }, @@ -2036,7 +2036,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.events.aggregate_process', { defaultMessage: - 'Reduce event volume by merging related process events into fewer aggregate events. Default is true.', + 'Reduce event volume by merging related process events into fewer aggregate events. Default is false.', } ), }, @@ -2047,7 +2047,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.alerts.hash.md5', { defaultMessage: - 'Compute and include MD5 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: false', + 'Compute and include MD5 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: true', } ), }, @@ -2058,7 +2058,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.alerts.hash.sha1', { defaultMessage: - 'Compute and include SHA-1 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: false', + 'Compute and include SHA-1 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: true', } ), }, @@ -2069,7 +2069,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.events.hash.md5', { defaultMessage: - 'Compute and include MD5 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: false', + 'Compute and include MD5 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: true', } ), }, @@ -2080,7 +2080,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.events.hash.sha1', { defaultMessage: - 'Compute and include SHA-1 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: false', + 'Compute and include SHA-1 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: true', } ), }, @@ -2102,7 +2102,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.alerts.hash.md5', { defaultMessage: - 'Compute and include MD5 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: false', + 'Compute and include MD5 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: true', } ), }, @@ -2113,7 +2113,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.alerts.hash.sha1', { defaultMessage: - 'Compute and include SHA-1 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: false', + 'Compute and include SHA-1 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: true', } ), }, @@ -2124,7 +2124,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.events.hash.md5', { defaultMessage: - 'Compute and include MD5 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: false', + 'Compute and include MD5 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: true', } ), }, @@ -2135,7 +2135,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.events.hash.sha1', { defaultMessage: - 'Compute and include SHA-1 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: false', + 'Compute and include SHA-1 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: true', } ), }, @@ -2157,7 +2157,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.alerts.hash.md5', { defaultMessage: - 'Compute and include MD5 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: false', + 'Compute and include MD5 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: true', } ), }, @@ -2168,7 +2168,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.alerts.hash.sha1', { defaultMessage: - 'Compute and include SHA-1 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: false', + 'Compute and include SHA-1 hashes in alerts? This will increase CPU usage and alert sizes. If any user exceptionlist, trustlist, or blocklists reference this hash type, Endpoint will ignore this setting and automatically enable this hash type. Default: true', } ), }, @@ -2179,7 +2179,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.events.hash.md5', { defaultMessage: - 'Compute and include MD5 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: false', + 'Compute and include MD5 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: true', } ), }, @@ -2190,7 +2190,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.events.hash.sha1', { defaultMessage: - 'Compute and include SHA-1 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: false', + 'Compute and include SHA-1 hashes for processes and libraries in events? This will increase CPU usage and event sizes. Default: true', } ), }, @@ -2212,7 +2212,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.set_extended_host_information', { defaultMessage: - 'Include more details about hosts in events? Set to false to receive only id, name and os. Setting to true will increase event size. Default: false', + 'Include more details about hosts in events? Set to false to receive only id, name and os. Setting to true will increase event size. Default: true', } ), }, @@ -2223,7 +2223,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.set_extended_host_information', { defaultMessage: - 'Include more details about hosts in events? Set to false to receive only id, name and os. Setting to true will increase event size. Default: false', + 'Include more details about hosts in events? Set to false to receive only id, name and os. Setting to true will increase event size. Default: true', } ), }, @@ -2234,7 +2234,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.set_extended_host_information', { defaultMessage: - 'Include more details about hosts in events? Set to false to receive only id, name and os. Setting to true will increase event size. Default: false', + 'Include more details about hosts in events? Set to false to receive only id, name and os. Setting to true will increase event size. Default: true', } ), }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_merging_banner.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_merging_banner.tsx index 26b229e219c2e..ec76a416e390d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_merging_banner.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/components/event_merging_banner.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ENDPOINT_VERSION_SUPPORTING_EVENT_MERGING_BY_DEFAULT } from '../constants'; export interface EventMergingBannerProps { onDismiss: () => void; @@ -29,7 +30,7 @@ export const EventMergingBanner = memo(({ onDismiss }) (({ onDismiss }) /> ), + minVersion: ENDPOINT_VERSION_SUPPORTING_EVENT_MERGING_BY_DEFAULT, }} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/constants.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/constants.ts new file mode 100644 index 0000000000000..19f1e36163aad --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Event merging banner is hidden temporarily for 8.16 (and serverless). + * Probably will be enabled for 8.17 or 8.18, when we can change the defaults and trigger policy deploy by migration. + * Blocker issue: https://github.com/elastic/kibana/issues/193352 + */ +export const ALLOW_SHOWING_EVENT_MERGING_BANNER = false; + +/** + * The version from which we decrease event volume by default. + */ +export const ENDPOINT_VERSION_SUPPORTING_EVENT_MERGING_BY_DEFAULT = '8.17'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx index d2e4c58adfb56..19a5ec8e6df39 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.test.tsx @@ -27,6 +27,13 @@ import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; jest.mock('../../../../../common/hooks/use_license'); +const mockAllowShowingEventMergingBannerConstantGetter = jest.fn(); +jest.mock('./constants', () => ({ + get ALLOW_SHOWING_EVENT_MERGING_BANNER() { + return mockAllowShowingEventMergingBannerConstantGetter(); + }, +})); + describe('Endpoint Policy Settings Form', () => { const testSubj = getPolicySettingsFormTestSubjects('test'); @@ -50,10 +57,23 @@ describe('Endpoint Policy Settings Form', () => { 'data-test-subj': 'test', }; + mockAllowShowingEventMergingBannerConstantGetter.mockReturnValue(false); + render = () => (renderResult = mockedContext.render()); }); describe('event merging banner', () => { + beforeEach(() => { + mockAllowShowingEventMergingBannerConstantGetter.mockReturnValue(true); + }); + + it('should hide the banner if its not allowed to be displayed', () => { + mockAllowShowingEventMergingBannerConstantGetter.mockReturnValue(false); + + render(); + + expect(renderResult.queryByTestId('eventMergingCallout')).not.toBeInTheDocument(); + }); it('should show the event merging banner for 8.16 if it has never been dismissed', () => { render(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx index 7d83170df2d26..619ba07346f47 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_settings_form/policy_settings_form.tsx @@ -26,6 +26,7 @@ import { MalwareProtectionsCard } from './components/cards/malware_protections_c import type { PolicyFormComponentCommonProps } from './types'; import { AdvancedSection } from './components/advanced_section'; import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator'; +import { ALLOW_SHOWING_EVENT_MERGING_BANNER } from './constants'; const PROTECTIONS_SECTION_TITLE = i18n.translate( 'xpack.securitySolution.endpoint.policy.details.protections', @@ -45,7 +46,8 @@ export const PolicySettingsForm = memo((props) => { const { storage } = useKibana().services; const [showEventMergingBanner, setShowEventMergingBanner] = useState( - storage.get('securitySolution.showEventMergingBanner') ?? true + ALLOW_SHOWING_EVENT_MERGING_BANNER && + (storage.get('securitySolution.showEventMergingBanner') ?? true) ); const onBannerDismiss = useCallback(() => { setShowEventMergingBanner(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx index e2aa8c42511d7..eae2eec549dfe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/session/use_session_view.tsx @@ -10,7 +10,6 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table'; -import type { TableId } from '@kbn/securitysolution-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys'; import { useSourcererDataView } from '../../../../../sourcerer/containers'; @@ -32,7 +31,6 @@ import { useTimelineFullScreen, useGlobalFullScreen, } from '../../../../../common/containers/use_full_screen'; -import { detectionsTimelineIds } from '../../../../containers/helpers'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { timelineActions, timelineSelectors } from '../../../../store'; import { timelineDefaults } from '../../../../store/defaults'; @@ -273,18 +271,8 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: [globalFullScreen, scopeId, timelineFullScreen] ); - const sourcererScope = useMemo(() => { - if (isActiveTimeline(scopeId)) { - return SourcererScopeName.timeline; - } else if (detectionsTimelineIds.includes(scopeId as TableId)) { - return SourcererScopeName.detections; - } else { - return SourcererScopeName.default; - } - }, [scopeId]); - - const { selectedPatterns } = useSourcererDataView(sourcererScope); - const eventDetailsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]); + const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + const alertsIndex = useMemo(() => selectedPatterns.join(','), [selectedPatterns]); const { openFlyout } = useExpandableFlyoutApi(); const openAlertDetailsFlyout = useCallback( @@ -294,7 +282,7 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: id: DocumentDetailsRightPanelKey, params: { id: eventId, - indexName: eventDetailsIndex, + indexName: alertsIndex, scopeId, }, }, @@ -304,7 +292,7 @@ export const useSessionView = ({ scopeId, height }: { scopeId: string; height?: panel: 'right', }); }, - [openFlyout, eventDetailsIndex, scopeId, telemetry] + [openFlyout, alertsIndex, scopeId, telemetry] ); const sessionViewComponent = useMemo(() => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_field_predefined_value.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_field_predefined_value.ts index 777711e56470c..2fbb6d427158d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_field_predefined_value.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/get_field_predefined_value.ts @@ -7,7 +7,7 @@ import { FIELDS_TO_UPGRADE_TO_CURRENT_VERSION, - NON_UPGRADEABLE_DIFFABLE_FIELDS, + FIELDS_TO_UPGRADE_TO_TARGET_VERSION, } from '../../../../../../common/api/detection_engine'; import { type PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset'; import type { RuleTriad } from '../../model/rule_groups/get_rule_groups'; @@ -24,10 +24,10 @@ type GetFieldPredefinedValueReturnType = * a predefined value or is customizable), and returns the value if it is predefined. * * This function checks whether a field can be upgraded via API contract and how it should - * be handled during the rule upgrade process. It uses the `NON_UPGRADEABLE_DIFFABLE_FIELDS` and + * be handled during the rule upgrade process. It uses the `FIELDS_TO_UPGRADE_TO_TARGET_VERSION` and * `FIELDS_TO_UPGRADE_TO_CURRENT_VERSION` constants to make this determination. * - * `NON_UPGRADEABLE_DIFFABLE_FIELDS` includes fields that are not upgradeable: 'type', 'rule_id', + * `FIELDS_TO_UPGRADE_TO_TARGET_VERSION` includes fields that are not upgradeable: 'type', 'rule_id', * 'version', 'author', and 'license', and are always upgraded to the target version. * * `FIELDS_TO_UPGRADE_TO_CURRENT_VERSION` includes fields that should be updated to their @@ -46,8 +46,8 @@ export const getFieldPredefinedValue = ( upgradeableRule: RuleTriad ): GetFieldPredefinedValueReturnType => { if ( - NON_UPGRADEABLE_DIFFABLE_FIELDS.includes( - fieldName as (typeof NON_UPGRADEABLE_DIFFABLE_FIELDS)[number] + FIELDS_TO_UPGRADE_TO_TARGET_VERSION.includes( + fieldName as (typeof FIELDS_TO_UPGRADE_TO_TARGET_VERSION)[number] ) ) { return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts index b49e04f566c4b..bde52596667d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/logic/diff/calculation/calculate_rule_fields_diff.ts @@ -194,10 +194,7 @@ const commonFieldsDiffAlgorithms: FieldsDiffAlgorithmsFor setup: multiLineStringDiffAlgorithm, related_integrations: simpleDiffAlgorithm, required_fields: simpleDiffAlgorithm, - author: scalarArrayDiffAlgorithm, - license: singleLineStringDiffAlgorithm, rule_schedule: simpleDiffAlgorithm, - exceptions_list: simpleDiffAlgorithm, max_signals: numberDiffAlgorithm, rule_name_override: simpleDiffAlgorithm, timestamp_override: simpleDiffAlgorithm, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts index 173d722d782a1..0adc9c1d77d3d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts @@ -30,6 +30,7 @@ import { buildReasonMessageForEsqlAlert } from '../utils/reason_formatters'; import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { CreateRuleOptions, RunOpts, SignalSource } from '../types'; import { logEsqlRequest } from '../utils/logged_requests'; +import { getDataTierFilter } from '../utils/get_data_tier_filter'; import * as i18n from '../translations'; import { @@ -90,6 +91,10 @@ export const esqlExecutor = async ({ return withSecuritySpan('esqlExecutor', async () => { const result = createSearchAfterReturnType(); let size = tuple.maxSignals; + const dataTiersFilters = await getDataTierFilter({ + uiSettingsClient: services.uiSettingsClient, + }); + try { while ( result.createdSignalsCount <= tuple.maxSignals && @@ -100,7 +105,7 @@ export const esqlExecutor = async ({ from: tuple.from.toISOString(), to: tuple.to.toISOString(), size, - filters: [], + filters: dataTiersFilters, primaryTimestamp, secondaryTimestamp, exceptionFilter, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts index 4f051c48dcd6c..b957030f2c8e5 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_data_client.ts @@ -342,6 +342,7 @@ export class AssetCriticalityDataClient { asset: { criticality: CRITICALITY_VALUES.DELETED, }, + '@timestamp': new Date().toISOString(), ...getImplicitEntityFields({ ...idParts, criticalityLevel: CRITICALITY_VALUES.DELETED, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts index 6699e160634fd..32cb52a61d469 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts @@ -48,9 +48,7 @@ export const getUnitedEntityDefinition = memoize( namespace, indexPatterns, }); - }, - ({ entityType, namespace, fieldHistoryLength }: Options) => - `${entityType}-${namespace}-${fieldHistoryLength}` + } ); export const getUnitedEntityDefinitionVersion = (entityType: EntityType): string => diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts index e30c067a0d4a4..29ef513b40bb3 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts @@ -62,7 +62,7 @@ export class ProductFeaturesService { casesFeature.baseKibanaSubFeatureIds ); - const assistantFeature = getAssistantFeature(); + const assistantFeature = getAssistantFeature(this.experimentalFeatures); this.securityAssistantProductFeatures = new ProductFeatures( this.logger, assistantFeature.subFeaturesMap, diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx index bb55cea5cd50f..f586f0d7f035e 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx @@ -19,12 +19,13 @@ import { import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceContentTab } from './edit_space_content_tab'; -import { EditSpaceProvider } from './provider'; +import { EditSpaceProviderRoot } from './provider'; import type { Space } from '../../../common'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import type { SpaceContentTypeSummaryItem } from '../../types'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); @@ -42,7 +43,7 @@ const logger = loggingSystemMock.createLogger(); const TestComponent: React.FC = ({ children }) => { return ( - = ({ children }) => { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx index 1c32b97f777c0..9a35572254340 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx @@ -23,12 +23,13 @@ import { KibanaFeature } from '@kbn/features-plugin/common'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceSettingsTab } from './edit_space_general_tab'; -import { EditSpaceProvider } from './provider/edit_space_provider'; +import { EditSpaceProviderRoot } from './provider/edit_space_provider'; import type { SolutionView } from '../../../common'; import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const space = { id: 'default', name: 'Default', disabledFeatures: [], _reserved: true }; const history = scopedHistoryMock.create(); @@ -64,7 +65,7 @@ describe('EditSpaceSettings', () => { const TestComponent: React.FC = ({ children }) => { return ( - { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx index 882301d36459a..bf59f00b5490d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx @@ -9,9 +9,9 @@ import React from 'react'; import type { ComponentProps, PropsWithChildren } from 'react'; import { EditSpace } from './edit_space'; -import { EditSpaceProvider, type EditSpaceProviderProps } from './provider'; +import { EditSpaceProviderRoot, type EditSpaceProviderRootProps } from './provider'; -type EditSpacePageProps = ComponentProps & EditSpaceProviderProps; +type EditSpacePageProps = ComponentProps & EditSpaceProviderRootProps; export function EditSpacePage({ spaceId, @@ -25,7 +25,7 @@ export function EditSpacePage({ ...editSpaceServicesProps }: PropsWithChildren) { return ( - + - + ); } diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx index fccd999eb7941..1959d1d8465ac 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx @@ -19,10 +19,11 @@ import { import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceAssignedRolesTab } from './edit_space_roles_tab'; -import { EditSpaceProvider } from './provider'; +import { EditSpaceProviderRoot } from './provider'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); @@ -51,7 +52,7 @@ describe('EditSpaceAssignedRolesTab', () => { const TestComponent: React.FC = ({ children }) => { return ( - { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx index 2733790d8de8b..2e3d40527dbd7 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx @@ -62,7 +62,7 @@ export const EditSpaceAssignedRolesTab: FC = ({ space, features, isReadOn (defaultSelected?: Role[]) => { const overlayRef = overlays.openFlyout( toMountPoint( - + = ({ space, features, isReadOn [ overlays, services, + dispatch, + state, space, features, - dispatch, invokeClient, getUrlForApp, theme, diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx index bfd7d7b6059e8..a236b9bc05e1d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx @@ -20,10 +20,15 @@ import { import type { ApplicationStart } from '@kbn/core-application-browser'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider'; +import { + EditSpaceProviderRoot, + useEditSpaceServices, + useEditSpaceStore, +} from './edit_space_provider'; import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../../security_license.mock'; const http = httpServiceMock.createStartContract(); const notifications = notificationServiceMock.createStartContract(); @@ -45,7 +50,7 @@ const SUTProvider = ({ }: PropsWithChildren>>) => { return ( - _, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: getPrivilegeAPIClientMock, + getSecurityLicense: getSecurityLicenseMock, navigateToUrl: jest.fn(), capabilities, }} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx index 75af2beea2108..374d90d19ace1 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx @@ -23,6 +23,7 @@ import type { Logger } from '@kbn/logging'; import type { PrivilegesAPIClientPublicContract, RolesAPIClient, + SecurityLicense, } from '@kbn/security-plugin-types-public'; import { @@ -32,7 +33,7 @@ import { } from './reducers'; import type { SpacesManager } from '../../../spaces_manager'; -export interface EditSpaceProviderProps +export interface EditSpaceProviderRootProps extends Pick { logger: Logger; capabilities: ApplicationStart['capabilities']; @@ -42,10 +43,7 @@ export interface EditSpaceProviderProps spacesManager: SpacesManager; getRolesAPIClient: () => Promise; getPrivilegesAPIClient: () => Promise; -} - -export interface EditSpaceServices extends EditSpaceProviderProps { - invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; + getSecurityLicense: () => Promise; } interface EditSpaceClients { @@ -54,6 +52,15 @@ interface EditSpaceClients { privilegesClient: PrivilegesAPIClientPublicContract; } +export interface EditSpaceServices + extends Omit< + EditSpaceProviderRootProps, + 'getRolesAPIClient' | 'getPrivilegesAPIClient' | 'getSecurityLicense' + > { + invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; + license?: SecurityLicense; +} + export interface EditSpaceStore { state: IEditSpaceStoreState; dispatch: Dispatch; @@ -63,16 +70,43 @@ const createSpaceRolesContext = once(() => createContext( const createEditSpaceServicesContext = once(() => createContext(null)); +/** + * + * @description EditSpaceProvider is a provider component that wraps the children components with the necessary context providers for the Edit Space feature. It provides the necessary services and state management for the feature, + * this is provided as an export for use with out of band renders within the spaces app + */ export const EditSpaceProvider = ({ children, + state, + dispatch, ...services -}: PropsWithChildren) => { +}: PropsWithChildren) => { const EditSpaceStoreContext = createSpaceRolesContext(); const EditSpaceServicesContext = createEditSpaceServicesContext(); - const clients = useRef( - Promise.all([services.getRolesAPIClient(), services.getPrivilegesAPIClient()]) + return ( + + + {children} + + ); +}; + +/** + * @description EditSpaceProviderRoot is the root provider for the Edit Space feature. It instantiates the necessary services and state management for the feature. It ideally + * should only be rendered once + */ +export const EditSpaceProviderRoot = ({ + children, + ...services +}: PropsWithChildren) => { + const { logger, getRolesAPIClient, getPrivilegesAPIClient, getSecurityLicense } = services; + + const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()])); + const license = useRef(getSecurityLicense); + + const licenseRef = useRef(); const rolesAPIClientRef = useRef(); const privilegesClientRef = useRef(); @@ -81,7 +115,14 @@ export const EditSpaceProvider = ({ fetchRolesError: false, }); - const { logger } = services; + const resolveSecurityLicense = useCallback(async () => { + try { + licenseRef.current = await license.current(); + } catch (err) { + logger.error('Could not resolve Security License!', err); + } + }, [logger]); + const resolveAPIClients = useCallback(async () => { try { [rolesAPIClientRef.current, privilegesClientRef.current] = await clients.current; @@ -94,6 +135,10 @@ export const EditSpaceProvider = ({ resolveAPIClients(); }, [resolveAPIClients]); + useEffect(() => { + resolveSecurityLicense(); + }, [resolveSecurityLicense]); + const createInitialState = useCallback((state: IEditSpaceStoreState) => { return state; }, []); @@ -118,11 +163,11 @@ export const EditSpaceProvider = ({ ); return ( - - - {children} - - + + {children} + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts index 7ae7301cd2c60..405f59c44a6f8 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts @@ -5,9 +5,14 @@ * 2.0. */ -export { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider'; +export { + EditSpaceProviderRoot, + EditSpaceProvider, + useEditSpaceServices, + useEditSpaceStore, +} from './edit_space_provider'; export type { - EditSpaceProviderProps, + EditSpaceProviderRootProps, EditSpaceServices, EditSpaceStore, } from './edit_space_provider'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index 3595cefd1220c..7f99202e23791 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import crypto from 'crypto'; import React from 'react'; @@ -19,7 +19,7 @@ import { themeServiceMock, } from '@kbn/core/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import type { Role } from '@kbn/security-plugin-types-common'; +import type { Role, SecurityLicense } from '@kbn/security-plugin-types-common'; import { createRawKibanaPrivileges, kibanaFeatures, @@ -33,11 +33,8 @@ import { FEATURE_PRIVILEGES_READ, } from '../../../../../common/constants'; import { spacesManagerMock } from '../../../../spaces_manager/spaces_manager.mock'; -import { - createPrivilegeAPIClientMock, - getPrivilegeAPIClientMock, -} from '../../../privilege_api_client.mock'; -import { createRolesAPIClientMock, getRolesAPIClientMock } from '../../../roles_api_client.mock'; +import { createPrivilegeAPIClientMock } from '../../../privilege_api_client.mock'; +import { createRolesAPIClientMock } from '../../../roles_api_client.mock'; import { EditSpaceProvider } from '../../provider'; const rolesAPIClient = createRolesAPIClientMock(); @@ -74,6 +71,9 @@ const spacesClientsInvocatorMock = jest.fn((fn) => const dispatchMock = jest.fn(); const onSaveCompleted = jest.fn(); const closeFlyout = jest.fn(); +const licenseMock = { + getFeatures: jest.fn(() => ({})), +} as unknown as SecurityLicense; const renderPrivilegeRolesForm = ({ preSelectedRoles, @@ -93,15 +93,20 @@ const renderPrivilegeRolesForm = ({ spacesManager, serverBasePath: '', getUrlForApp: jest.fn((_) => _), - getRolesAPIClient: getRolesAPIClientMock, - getPrivilegesAPIClient: getPrivilegeAPIClientMock, navigateToUrl: jest.fn(), + license: licenseMock, capabilities: { navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true }, }, + dispatch: dispatchMock, + state: { + roles: new Map(), + fetchRolesError: false, + }, + invokeClient: spacesClientsInvocatorMock, }} > _), }} /> @@ -358,11 +360,11 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); - - expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( - 'aria-pressed', - String(true) + await waitFor(() => + expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( + 'aria-pressed', + String(true) + ) ); await user.click(screen.getByTestId('custom-privilege-button')); @@ -408,5 +410,116 @@ describe('PrivilegesRolesForm', () => { String(true) ); }); + + it('prevents customization up to sub privilege level by default', async () => { + const user = userEvent.setup(); + + const roles: Role[] = [ + createRole('test_role_1', [ + { base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] }, + ]), + ]; + + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) => + Boolean(kibanaFeature.subFeatures.length) + ); + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await user.click(screen.getByTestId('custom-privilege-button')); + + expect( + screen.getByTestId('space-assign-role-privilege-customization-form') + ).toBeInTheDocument(); + + const featureUT = featuresWithSubFeatures[0]; + + // change a single feature with sub features to read from default privilege "none" + await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`)); + + // click on the accordion toggle to show sub features + await user.click( + screen.getByTestId( + `featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle` + ) + ); + + // sub feature table renders + expect( + screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`) + ).toBeInTheDocument(); + + // assert switch to customize sub feature can toggled + expect( + within( + screen.getByTestId( + `${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer` + ) + ).getByTestId('customizeSubFeaturePrivileges') + ).toBeDisabled(); + }); + + it('supports customization up to sub privilege level only when security license allows', async () => { + const user = userEvent.setup(); + + const roles: Role[] = [ + createRole('test_role_1', [ + { base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] }, + ]), + ]; + + // enable sub feature privileges + (licenseMock.getFeatures as jest.Mock).mockReturnValue({ + allowSubFeaturePrivileges: true, + }); + + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) => + Boolean(kibanaFeature.subFeatures.length) + ); + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await user.click(screen.getByTestId('custom-privilege-button')); + + expect( + screen.getByTestId('space-assign-role-privilege-customization-form') + ).toBeInTheDocument(); + + const featureUT = featuresWithSubFeatures[0]; + + // change a single feature with sub features to read from default privilege "none" + await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`)); + + // click on the accordion toggle to show sub features + await user.click( + screen.getByTestId( + `featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle` + ) + ); + + // sub feature table renders + expect( + screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`) + ).toBeInTheDocument(); + + // assert switch to customize sub feature can toggled + expect( + within( + screen.getByTestId( + `${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer` + ) + ).getByTestId('customizeSubFeaturePrivileges') + ).not.toBeDisabled(); + }); }); }); diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index f33c2cba25268..e0f3e8f3714c6 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -46,7 +46,7 @@ import { FEATURE_PRIVILEGES_CUSTOM, FEATURE_PRIVILEGES_READ, } from '../../../../../common/constants'; -import { type EditSpaceServices, type EditSpaceStore, useEditSpaceServices } from '../../provider'; +import { useEditSpaceServices, useEditSpaceStore } from '../../provider'; type KibanaRolePrivilege = | keyof NonNullable @@ -62,9 +62,6 @@ interface PrivilegesRolesFormProps { * this is useful when the form is opened in edit mode */ defaultSelected?: Role[]; - storeDispatch: EditSpaceStore['dispatch']; - spacesClientsInvocator: EditSpaceServices['invokeClient']; - getUrlForApp: EditSpaceServices['getUrlForApp']; } const createRolesComboBoxOptions = (roles: Role[]): Array> => @@ -74,17 +71,9 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { - const { - space, - onSaveCompleted, - closeFlyout, - features, - defaultSelected = [], - spacesClientsInvocator, - storeDispatch, - getUrlForApp, - } = props; - const { logger, notifications } = useEditSpaceServices(); + const { space, onSaveCompleted, closeFlyout, features, defaultSelected = [] } = props; + const { logger, notifications, license, invokeClient, getUrlForApp } = useEditSpaceServices(); + const { dispatch: storeDispatch } = useEditSpaceStore(); const [assigningToRole, setAssigningToRole] = useState(false); const [fetchingDataDeps, setFetchingDataDeps] = useState(false); const [kibanaPrivileges, setKibanaPrivileges] = useState(null); @@ -98,7 +87,7 @@ export const PrivilegesRolesForm: FC = (props) => { async function fetchRequiredData(spaceId: string) { setFetchingDataDeps(true); - const [systemRoles, _kibanaPrivileges] = await spacesClientsInvocator((clients) => + const [systemRoles, _kibanaPrivileges] = await invokeClient((clients) => Promise.all([ clients.rolesClient.getRoles(), clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }), @@ -123,7 +112,7 @@ export const PrivilegesRolesForm: FC = (props) => { } fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); - }, [space.id, spacesClientsInvocator]); + }, [invokeClient, space.id]); const selectedRolesCombinedPrivileges = useMemo(() => { const combinedPrivilege = new Set( @@ -315,7 +304,7 @@ export const PrivilegesRolesForm: FC = (props) => { return selectedRole.value!; }); - await spacesClientsInvocator((clients) => + await invokeClient((clients) => clients.rolesClient.bulkUpdateRoles({ rolesUpdate: updatedRoles }).then((response) => { setAssigningToRole(false); onSaveCompleted(response); @@ -338,13 +327,14 @@ export const PrivilegesRolesForm: FC = (props) => { }); } }, [ + roleSpacePrivilege, + roleCustomizationAnchor.value?.kibana, + roleCustomizationAnchor.privilegeIndex, selectedRoles, - spacesClientsInvocator, + invokeClient, storeDispatch, - onSaveCompleted, space.id, - roleSpacePrivilege, - roleCustomizationAnchor, + onSaveCompleted, logger, notifications.toasts, ]); @@ -571,7 +561,9 @@ export const PrivilegesRolesForm: FC = (props) => { ) } allSpacesSelected={false} - canCustomizeSubFeaturePrivileges={false} + canCustomizeSubFeaturePrivileges={ + license?.getFeatures().allowSubFeaturePrivileges ?? false + } /> )} diff --git a/x-pack/plugins/spaces/public/management/management_service.test.ts b/x-pack/plugins/spaces/public/management/management_service.test.ts index 40a61397e286f..c01c6ecf2e9b3 100644 --- a/x-pack/plugins/spaces/public/management/management_service.test.ts +++ b/x-pack/plugins/spaces/public/management/management_service.test.ts @@ -13,6 +13,7 @@ import { managementPluginMock } from '@kbn/management-plugin/public/mocks'; import { ManagementService } from './management_service'; import { getRolesAPIClientMock } from './roles_api_client.mock'; +import { getSecurityLicenseMock } from './security_license.mock'; import { EventTracker } from '../analytics'; import type { ConfigType } from '../config'; import type { PluginsStart } from '../plugin'; @@ -49,6 +50,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, }); @@ -72,6 +74,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, }); }); @@ -96,6 +99,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, }); diff --git a/x-pack/plugins/spaces/public/management/management_service.tsx b/x-pack/plugins/spaces/public/management/management_service.tsx index 0379189e192c3..908803728edd6 100644 --- a/x-pack/plugins/spaces/public/management/management_service.tsx +++ b/x-pack/plugins/spaces/public/management/management_service.tsx @@ -5,29 +5,15 @@ * 2.0. */ -import type { StartServicesAccessor } from '@kbn/core/public'; -import type { Logger } from '@kbn/logging'; import type { ManagementApp, ManagementSetup } from '@kbn/management-plugin/public'; -import type { - PrivilegesAPIClientPublicContract, - RolesAPIClient, -} from '@kbn/security-plugin-types-public'; -import { spacesManagementApp } from './spaces_management_app'; -import type { EventTracker } from '../analytics'; -import type { ConfigType } from '../config'; -import type { PluginsStart } from '../plugin'; -import type { SpacesManager } from '../spaces_manager'; +import { + spacesManagementApp, + type CreateParams as SpacesManagementAppCreateParams, +} from './spaces_management_app'; -interface SetupDeps { +interface SetupDeps extends SpacesManagementAppCreateParams { management: ManagementSetup; - getStartServices: StartServicesAccessor; - spacesManager: SpacesManager; - config: ConfigType; - getRolesAPIClient: () => Promise; - eventTracker: EventTracker; - getPrivilegesAPIClient: () => Promise; - logger: Logger; } export class ManagementService { @@ -42,6 +28,7 @@ export class ManagementService { getRolesAPIClient, eventTracker, getPrivilegesAPIClient, + getSecurityLicense, }: SetupDeps) { this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp( spacesManagementApp.create({ @@ -52,6 +39,7 @@ export class ManagementService { getRolesAPIClient, eventTracker, getPrivilegesAPIClient, + getSecurityLicense, }) ); } diff --git a/x-pack/plugins/spaces/public/management/security_license.mock.ts b/x-pack/plugins/spaces/public/management/security_license.mock.ts new file mode 100644 index 0000000000000..d5d6e73d03db4 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/security_license.mock.ts @@ -0,0 +1,49 @@ +/* + * 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 { BehaviorSubject, type Observable } from 'rxjs'; + +import type { SecurityLicense } from '@kbn/security-plugin-types-public'; + +type SecurityLicenseFeatures = SecurityLicense['features$'] extends Observable ? P : never; + +export const createSecurityLicenseMock = ({ + securityFeaturesConfig, +}: { + securityFeaturesConfig: SecurityLicenseFeatures; +}): SecurityLicense => { + return { + isLicenseAvailable: jest.fn(), + isEnabled: jest.fn(), + getFeatures: jest.fn(), + getUnavailableReason: jest.fn(), + hasAtLeast: jest.fn(), + getLicenseType: jest.fn(), + features$: new BehaviorSubject(securityFeaturesConfig), + }; +}; + +export const getSecurityLicenseMock = jest.fn().mockResolvedValue( + createSecurityLicenseMock({ + securityFeaturesConfig: { + showLinks: true, + showLogin: true, + allowLogin: true, + allowRbac: true, + allowFips: true, + showRoleMappingsManagement: true, + allowAccessAgreement: true, + allowAuditLogging: true, + allowSubFeaturePrivileges: true, + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + allowRoleRemoteIndexPrivileges: true, + allowRemoteClusterPrivileges: true, + allowUserProfileCollaboration: true, + }, + }) +); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index a04335613e59b..dc887a7b85f13 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -77,6 +77,7 @@ async function mountApp(basePath: string, pathname: string, spaceId?: string) { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: jest.fn(), eventTracker, }) .mount({ @@ -101,6 +102,7 @@ describe('spacesManagementApp', () => { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: jest.fn(), eventTracker, }) ).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index fa74316779a7e..b5cc476f9a527 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -18,6 +18,7 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { PrivilegesAPIClientPublicContract, RolesAPIClient, + SecurityLicense, } from '@kbn/security-plugin-types-public'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; @@ -28,7 +29,7 @@ import type { ConfigType } from '../config'; import type { PluginsStart } from '../plugin'; import type { SpacesManager } from '../spaces_manager'; -interface CreateParams { +export interface CreateParams { getStartServices: StartServicesAccessor; spacesManager: SpacesManager; config: ConfigType; @@ -36,6 +37,7 @@ interface CreateParams { getRolesAPIClient: () => Promise; eventTracker: EventTracker; getPrivilegesAPIClient: () => Promise; + getSecurityLicense: () => Promise; } export const spacesManagementApp = Object.freeze({ @@ -48,6 +50,7 @@ export const spacesManagementApp = Object.freeze({ eventTracker, getRolesAPIClient, getPrivilegesAPIClient, + getSecurityLicense, }: CreateParams) { const title = i18n.translate('xpack.spaces.displayName', { defaultMessage: 'Spaces', @@ -146,6 +149,7 @@ export const spacesManagementApp = Object.freeze({ capabilities={application.capabilities} getUrlForApp={application.getUrlForApp} navigateToUrl={application.navigateToUrl} + getSecurityLicense={getSecurityLicense} serverBasePath={http.basePath.serverBasePath} getFeatures={features.getFeatures} http={http} diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 86196333c0883..595b892dfe34d 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -10,7 +10,7 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb import type { FeaturesPluginStart } from '@kbn/features-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; -import type { SecurityPluginStart } from '@kbn/security-plugin-types-public'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin-types-public'; import { EventTracker, registerAnalyticsContext, registerSpacesEventTypes } from './analytics'; import type { ConfigType } from './config'; @@ -114,6 +114,18 @@ export class SpacesPlugin implements Plugin { + const { security } = await core.plugins.onSetup<{ security: SecurityPluginSetup }>( + 'security' + ); + + if (!security.found) { + throw new Error('Security plugin is not available as runtime dependency.'); + } + + return security.contract.license; + }; + if (plugins.home) { plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); } @@ -129,6 +141,7 @@ export class SpacesPlugin implements Plugin { @@ -29,9 +48,20 @@ const spaceSchema = schema.object({ return `must be a 6 digit hex color, starting with a #`; } }, + meta: { + description: + 'The hexadecimal color code used in the space avatar. By default, the color is automatically generated from the space name.', + }, }) ), - disabledFeatures: schema.arrayOf(schema.string(), { defaultValue: [] }), + disabledFeatures: schema.arrayOf( + schema.string({ + meta: { + description: 'The list of features that are turned off in the space.', + }, + }), + { defaultValue: [] } + ), _reserved: schema.maybe(schema.boolean()), imageUrl: schema.maybe( schema.string({ @@ -40,6 +70,10 @@ const spaceSchema = schema.object({ return `must start with 'data:image'`; } }, + meta: { + description: + 'The data-URL encoded image to display in the space avatar. If specified, initials will not be displayed and the color will be visible as the background color for transparent images. For best results, your image should be 64x64. Images will not be optimized by this API call, so care should be taken when using custom images.', + }, }) ), }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index f1f1f22b55e32..b0758f5645cc1 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -39,8 +39,10 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { path: '/api/spaces/_copy_saved_objects', options: { access: isServerless ? 'internal' : 'public', - tags: ['access:copySavedObjectsToSpaces'], - description: `Copy saved objects to spaces`, + tags: ['access:copySavedObjectsToSpaces', 'oas-tag:spaces'], + summary: `Copy saved objects between spaces`, + description: + 'It also allows you to automatically copy related objects, so when you copy a dashboard, this can automatically copy over the associated visualizations, data views, and saved searches, as required. You can request to overwrite any objects that already exist in the target space if they share an identifier or you can use the resolve copy saved objects conflicts API to do this on a per-object basis.', }, validate: { body: schema.object( @@ -52,6 +54,10 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { return `lower case, a-z, 0-9, "_", and "-" are allowed`; } }, + meta: { + description: + 'The identifiers of the spaces where you want to copy the specified objects.', + }, }), { validate: (spaceIds) => { @@ -63,8 +69,12 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { ), objects: schema.arrayOf( schema.object({ - type: schema.string(), - id: schema.string(), + type: schema.string({ + meta: { description: 'The type of the saved object to copy.' }, + }), + id: schema.string({ + meta: { description: 'The identifier of the saved object to copy.' }, + }), }), { validate: (objects) => { @@ -74,17 +84,40 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }, } ), - includeReferences: schema.boolean({ defaultValue: false }), - overwrite: schema.boolean({ defaultValue: false }), - createNewCopies: schema.boolean({ defaultValue: true }), - compatibilityMode: schema.boolean({ defaultValue: false }), + includeReferences: schema.boolean({ + defaultValue: false, + meta: { + description: + 'When set to true, all saved objects related to the specified saved objects will also be copied into the target spaces.', + }, + }), + overwrite: schema.boolean({ + defaultValue: false, + meta: { + description: + 'When set to true, all conflicts are automatically overridden. When a saved object with a matching type and identifier exists in the target space, that version is replaced with the version from the source space. This option cannot be used with the `createNewCopies` option.', + }, + }), + createNewCopies: schema.boolean({ + defaultValue: true, + meta: { + description: + 'Create new copies of saved objects, regenerate each object identifier, and reset the origin. When used, potential conflict errors are avoided. This option cannot be used with the `overwrite` and `compatibilityMode` options.', + }, + }), + compatibilityMode: schema.boolean({ + defaultValue: false, + meta: { + description: + 'Apply various adjustments to the saved objects that are being copied to maintain compatibility between different Kibana versions. Use this option only if you encounter issues with copied saved objects. This option cannot be used with the `createNewCopies` option.', + }, + }), }, { validate: (object) => { if (object.overwrite && object.createNewCopies) { return 'cannot use [overwrite] with [createNewCopies]'; } - if (object.compatibilityMode && object.createNewCopies) { return 'cannot use [compatibilityMode] with [createNewCopies]'; } @@ -158,13 +191,19 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { options: { access: isServerless ? 'internal' : 'public', tags: ['access:copySavedObjectsToSpaces'], - description: `Resolve conflicts copying saved objects`, + summary: `Resolve conflicts copying saved objects`, + description: + 'Overwrite saved objects that are returned as errors from the copy saved objects to space API.', }, validate: { body: schema.object( { retries: schema.recordOf( schema.string({ + meta: { + description: + 'The retry operations to attempt, which can specify how to resolve different types of errors. Object keys represent the target space identifiers.', + }, validate: (spaceId) => { if (!SPACE_ID_REGEX.test(spaceId)) { return `Invalid space id: ${spaceId}`; @@ -173,12 +212,38 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }), schema.arrayOf( schema.object({ - type: schema.string(), - id: schema.string(), - overwrite: schema.boolean({ defaultValue: false }), - destinationId: schema.maybe(schema.string()), - createNewCopy: schema.maybe(schema.boolean()), - ignoreMissingReferences: schema.maybe(schema.boolean()), + type: schema.string({ meta: { description: 'The saved object type.' } }), + id: schema.string({ meta: { description: 'The saved object identifier.' } }), + overwrite: schema.boolean({ + defaultValue: false, + meta: { + description: + 'When set to true, the saved object from the source space overwrites the conflicting object in the destination space.', + }, + }), + destinationId: schema.maybe( + schema.string({ + meta: { + description: + 'Specifies the destination identifier that the copied object should have, if different from the current identifier.', + }, + }) + ), + createNewCopy: schema.maybe( + schema.boolean({ + meta: { + description: + 'Creates new copies of the saved objects, regenerates each object ID, and resets the origin.', + }, + }) + ), + ignoreMissingReferences: schema.maybe( + schema.boolean({ + meta: { + description: 'When set to true, any missing references errors are ignored.', + }, + }) + ), }) ) ), diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index 515f8811e5dcf..06bef75774aa0 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -22,7 +22,8 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { .delete({ path: '/api/spaces/space/{id}', access: 'public', - description: `Delete a space`, + summary: `Delete a space`, + description: `When you delete a space, all saved objects that belong to the space are automatically deleted, which is permanent and cannot be undone.`, options: { tags: ['oas-tag:spaces'], }, @@ -33,9 +34,19 @@ export function initDeleteSpacesApi(deps: ExternalRouteDeps) { validate: { request: { params: schema.object({ - id: schema.string(), + id: schema.string({ + meta: { description: 'The space identifier.' }, + }), }), }, + response: { + 204: { + description: 'Indicates a successful call.', + }, + 404: { + description: 'Indicates that the request failed.', + }, + }, }, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts index 6b3c70eb64ffa..a1610bbfed975 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts @@ -20,15 +20,25 @@ export function initDisableLegacyUrlAliasesApi(deps: ExternalRouteDeps) { path: '/api/spaces/_disable_legacy_url_aliases', options: { access: isServerless ? 'internal' : 'public', - description: `Disable legacy URL aliases`, + summary: 'Disable legacy URL aliases', + tags: ['oas-tag:spaces'], }, validate: { body: schema.object({ aliases: schema.arrayOf( schema.object({ - targetSpace: schema.string(), - targetType: schema.string(), - sourceId: schema.string(), + targetSpace: schema.string({ + meta: { description: 'The space where the alias target object exists.' }, + }), + targetType: schema.string({ + meta: { description: 'The type of alias target object. ' }, + }), + sourceId: schema.string({ + meta: { + description: + 'The alias source object identifier. This is the legacy object identifier.', + }, + }), }) ), }), diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.ts b/x-pack/plugins/spaces/server/routes/api/external/get.ts index 8d4e3c0c359ef..b1ab2dc575774 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.ts @@ -20,7 +20,7 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) { .get({ path: '/api/spaces/space/{id}', access: 'public', - description: `Get a space`, + summary: `Get a space`, options: { tags: ['oas-tag:spaces'], }, @@ -31,9 +31,14 @@ export function initGetSpaceApi(deps: ExternalRouteDeps) { validate: { request: { params: schema.object({ - id: schema.string(), + id: schema.string({ meta: { description: 'The space identifier.' } }), }), }, + response: { + 200: { + description: 'Indicates a successful call.', + }, + }, }, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts index baa47ca6956dc..746735bb3736e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts @@ -19,7 +19,7 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) { .get({ path: '/api/spaces/space', access: 'public', - description: `Get all spaces`, + summary: `Get all spaces`, options: { tags: ['oas-tag:spaces'], }, @@ -31,20 +31,39 @@ export function initGetAllSpacesApi(deps: ExternalRouteDeps) { request: { query: schema.object({ purpose: schema.maybe( - schema.oneOf([ - schema.literal('any'), - schema.literal('copySavedObjectsIntoSpace'), - schema.literal('shareSavedObjectsIntoSpace'), - ]) + schema.oneOf( + [ + schema.literal('any'), + schema.literal('copySavedObjectsIntoSpace'), + schema.literal('shareSavedObjectsIntoSpace'), + ], + { + meta: { + description: + 'Specifies which authorization checks are applied to the API call. The default value is `any`.', + }, + } + ) ), include_authorized_purposes: schema.conditional( schema.siblingRef('purpose'), schema.string(), schema.maybe(schema.literal(false)), - schema.maybe(schema.boolean()) + schema.maybe(schema.boolean()), + { + meta: { + description: + 'When enabled, the API returns any spaces that the user is authorized to access in any capacity and each space will contain the purposes for which the user is authorized. This can be useful to determine which spaces a user can read but not take a specific action in. If the security plugin is not enabled, this parameter has no effect, since no authorization checks take place. This parameter cannot be used in with the `purpose` parameter.', + }, + } ), }), }, + response: { + 200: { + description: 'Indicates a successful call.', + }, + }, }, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts index 93a210cd82b3e..f49070be66fe2 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts @@ -19,7 +19,9 @@ export function initGetShareableReferencesApi(deps: ExternalRouteDeps) { path: '/api/spaces/_get_shareable_references', options: { access: isServerless ? 'internal' : 'public', - description: `Get shareable references`, + summary: `Get shareable references`, + tags: ['oas-tag:spaces'], + description: 'Collect references and space contexts for saved objects.', }, validate: { body: schema.object({ diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.ts b/x-pack/plugins/spaces/server/routes/api/external/post.ts index c47ea4ad5f9bf..de1ec53aaee44 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.ts @@ -22,7 +22,7 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) { .post({ path: '/api/spaces/space', access: 'public', - description: `Create a space`, + summary: `Create a space`, options: { tags: ['oas-tag:spaces'], }, @@ -34,6 +34,11 @@ export function initPostSpacesApi(deps: ExternalRouteDeps) { request: { body: getSpaceSchema(isServerless), }, + response: { + 200: { + description: 'Indicates a successful call.', + }, + }, }, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.ts b/x-pack/plugins/spaces/server/routes/api/external/put.ts index 10374dc94f600..740e81bac446e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -21,7 +21,7 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) { .put({ path: '/api/spaces/space/{id}', access: 'public', - description: `Update a space`, + summary: `Update a space`, options: { tags: ['oas-tag:spaces'], }, @@ -32,10 +32,20 @@ export function initPutSpacesApi(deps: ExternalRouteDeps) { validate: { request: { params: schema.object({ - id: schema.string(), + id: schema.string({ + meta: { + description: + 'The space identifier. You are unable to change the ID with the update operation.', + }, + }), }), body: getSpaceSchema(isServerless), }, + response: { + 200: { + description: 'Indicates a successful call.', + }, + }, }, }, createLicensedRouteHandler(async (context, request, response) => { diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts index 68b89d0934cf1..9fb2a8626a841 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts @@ -18,6 +18,10 @@ export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { const spacesSchema = schema.arrayOf( schema.string({ + meta: { + description: + 'The identifiers of the spaces the saved objects should be added to or removed from.', + }, validate: (value) => { if (value !== ALL_SPACES_ID && !SPACE_ID_REGEX.test(value)) { return `lower case, a-z, 0-9, "_", and "-" are allowed, OR "*"`; @@ -38,11 +42,22 @@ export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { path: '/api/spaces/_update_objects_spaces', options: { access: isServerless ? 'internal' : 'public', - description: `Update saved objects in spaces`, + summary: `Update saved objects in spaces`, + tags: ['oas-tag:spaces'], + description: 'Update one or more saved objects to add or remove them from some spaces.', }, validate: { body: schema.object({ - objects: schema.arrayOf(schema.object({ type: schema.string(), id: schema.string() })), + objects: schema.arrayOf( + schema.object({ + type: schema.string({ + meta: { description: 'The type of the saved object to update.' }, + }), + id: schema.string({ + meta: { description: 'The identifier of the saved object to update.' }, + }), + }) + ), spacesToAdd: spacesSchema, spacesToRemove: spacesSchema, }), diff --git a/x-pack/plugins/task_manager/server/lib/set_claim_strategy.test.ts b/x-pack/plugins/task_manager/server/lib/set_claim_strategy.test.ts index bb3d679299d33..993693c6ba5ab 100644 --- a/x-pack/plugins/task_manager/server/lib/set_claim_strategy.test.ts +++ b/x-pack/plugins/task_manager/server/lib/set_claim_strategy.test.ts @@ -71,61 +71,67 @@ describe('setClaimStrategy', () => { }); for (const isServerless of [true, false]) { for (const isCloud of [true, false]) { - for (const deploymentId of [undefined, deploymentIdMget, deploymentIdUpdateByQuery]) { - for (const configuredStrategy of [CLAIM_STRATEGY_MGET, CLAIM_STRATEGY_UPDATE_BY_QUERY]) { - test(`should return config as is when claim strategy is already defined: isServerless=${isServerless}, isCloud=${isCloud}, deploymentId=${deploymentId}`, () => { - const config = { - ...getConfigWithoutClaimStrategy(), - claim_strategy: configuredStrategy, - }; - - const returnedConfig = setClaimStrategy({ - config, - logger, - isCloud, - isServerless, - deploymentId, + for (const isElasticStaffOwned of [true, false]) { + for (const deploymentId of [undefined, deploymentIdMget, deploymentIdUpdateByQuery]) { + for (const configuredStrategy of [CLAIM_STRATEGY_MGET, CLAIM_STRATEGY_UPDATE_BY_QUERY]) { + test(`should return config as is when claim strategy is already defined: isServerless=${isServerless}, isCloud=${isCloud}, isElasticStaffOwned=${isElasticStaffOwned}, deploymentId=${deploymentId}`, () => { + const config = { + ...getConfigWithoutClaimStrategy(), + claim_strategy: configuredStrategy, + }; + + const returnedConfig = setClaimStrategy({ + config, + logger, + isCloud, + isServerless, + isElasticStaffOwned, + deploymentId, + }); + + expect(returnedConfig).toStrictEqual(config); + if (deploymentId) { + expect(logger.info).toHaveBeenCalledWith( + `Using claim strategy ${configuredStrategy} as configured for deployment ${deploymentId}` + ); + } else { + expect(logger.info).toHaveBeenCalledWith( + `Using claim strategy ${configuredStrategy} as configured` + ); + } }); - - expect(returnedConfig).toStrictEqual(config); - if (deploymentId) { - expect(logger.info).toHaveBeenCalledWith( - `Using claim strategy ${configuredStrategy} as configured for deployment ${deploymentId}` - ); - } else { - expect(logger.info).toHaveBeenCalledWith( - `Using claim strategy ${configuredStrategy} as configured` - ); - } - }); + } } } } } for (const isCloud of [true, false]) { - for (const deploymentId of [undefined, deploymentIdMget, deploymentIdUpdateByQuery]) { - test(`should set claim strategy to mget if in serverless: isCloud=${isCloud}, deploymentId=${deploymentId}`, () => { - const config = getConfigWithoutClaimStrategy(); - const returnedConfig = setClaimStrategy({ - config, - logger, - isCloud, - isServerless: true, - deploymentId, - }); + for (const isElasticStaffOwned of [true, false]) { + for (const deploymentId of [undefined, deploymentIdMget, deploymentIdUpdateByQuery]) { + test(`should set claim strategy to mget if in serverless: isCloud=${isCloud}, isElasticStaffOwned=${isElasticStaffOwned}, deploymentId=${deploymentId}`, () => { + const config = getConfigWithoutClaimStrategy(); + const returnedConfig = setClaimStrategy({ + config, + logger, + isCloud, + isServerless: true, + isElasticStaffOwned, + deploymentId, + }); - expect(returnedConfig.claim_strategy).toBe(CLAIM_STRATEGY_MGET); - expect(returnedConfig.poll_interval).toBe(MGET_DEFAULT_POLL_INTERVAL); + expect(returnedConfig.claim_strategy).toBe(CLAIM_STRATEGY_MGET); + expect(returnedConfig.poll_interval).toBe(MGET_DEFAULT_POLL_INTERVAL); - if (deploymentId) { - expect(logger.info).toHaveBeenCalledWith( - `Setting claim strategy to mget for serverless deployment ${deploymentId}` - ); - } else { - expect(logger.info).toHaveBeenCalledWith(`Setting claim strategy to mget`); - } - }); + if (deploymentId) { + expect(logger.info).toHaveBeenCalledWith( + `Setting claim strategy to mget for serverless deployment ${deploymentId}` + ); + } else { + expect(logger.info).toHaveBeenCalledWith(`Setting claim strategy to mget`); + } + }); + } } } @@ -135,6 +141,7 @@ describe('setClaimStrategy', () => { config, logger, isCloud: false, + isElasticStaffOwned: false, isServerless: false, }); @@ -150,6 +157,7 @@ describe('setClaimStrategy', () => { config, logger, isCloud: true, + isElasticStaffOwned: false, isServerless: false, }); @@ -165,6 +173,7 @@ describe('setClaimStrategy', () => { config, logger, isCloud: true, + isElasticStaffOwned: false, isServerless: false, deploymentId: deploymentIdUpdateByQuery, }); @@ -177,12 +186,32 @@ describe('setClaimStrategy', () => { ); }); + test(`should set claim strategy to mget if cloud, deploymentId does not start with a or b, not serverless and isElasticStaffOwned is true`, () => { + const config = getConfigWithoutClaimStrategy(); + const returnedConfig = setClaimStrategy({ + config, + logger, + isCloud: true, + isElasticStaffOwned: true, + isServerless: false, + deploymentId: deploymentIdUpdateByQuery, + }); + + expect(returnedConfig.claim_strategy).toBe(CLAIM_STRATEGY_MGET); + expect(returnedConfig.poll_interval).toBe(MGET_DEFAULT_POLL_INTERVAL); + + expect(logger.info).toHaveBeenCalledWith( + `Setting claim strategy to mget for deployment ${deploymentIdUpdateByQuery}` + ); + }); + test(`should set claim strategy to mget if cloud and not serverless and deploymentId starts with a or b`, () => { const config = getConfigWithoutClaimStrategy(); const returnedConfig = setClaimStrategy({ config, logger, isCloud: true, + isElasticStaffOwned: false, isServerless: false, deploymentId: deploymentIdMget, }); diff --git a/x-pack/plugins/task_manager/server/lib/set_claim_strategy.ts b/x-pack/plugins/task_manager/server/lib/set_claim_strategy.ts index 52d71d25c7387..9ff24ad67a963 100644 --- a/x-pack/plugins/task_manager/server/lib/set_claim_strategy.ts +++ b/x-pack/plugins/task_manager/server/lib/set_claim_strategy.ts @@ -19,6 +19,7 @@ interface SetClaimStrategyOpts { deploymentId?: string; isServerless: boolean; isCloud: boolean; + isElasticStaffOwned: boolean; logger: Logger; } @@ -50,7 +51,10 @@ export function setClaimStrategy(opts: SetClaimStrategyOpts): TaskManagerConfig let defaultToMget = false; if (opts.isCloud && !opts.isServerless && opts.deploymentId) { - defaultToMget = opts.deploymentId.startsWith('a') || opts.deploymentId.startsWith('b'); + defaultToMget = + opts.deploymentId.startsWith('a') || + opts.deploymentId.startsWith('b') || + opts.isElasticStaffOwned; if (defaultToMget) { opts.logger.info(`Setting claim strategy to mget for deployment ${opts.deploymentId}`); } else { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 61731c4ae82f3..3bf9d8e928ca8 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -136,6 +136,7 @@ export class TaskManagerPlugin deploymentId: plugins.cloud?.deploymentId, isServerless: this.initContext.env.packageInfo.buildFlavor === 'serverless', isCloud: plugins.cloud?.isCloudEnabled ?? false, + isElasticStaffOwned: plugins.cloud?.isElasticStaffOwned ?? false, logger: this.logger, }); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index bcee389d1d91b..92300d5580cbb 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -37,6 +37,8 @@ import { useStorage } from '@kbn/ml-local-storage'; import { useUrlState } from '@kbn/ml-url-state'; import { useFieldStatsFlyoutContext } from '@kbn/ml-field-stats-flyout'; +import { MAX_ROW_COUNT } from '@kbn/ml-data-grid/lib/common'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { PivotAggDict } from '../../../../../../common/types/pivot_aggs'; import type { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by'; import { TRANSFORM_FUNCTION } from '../../../../../../common/constants'; @@ -288,6 +290,14 @@ export const StepDefineForm: FC = React.memo((props) => { }; }); + const rowCountInfoLabel = ( + + ); + return (
@@ -467,6 +477,11 @@ export const StepDefineForm: FC = React.memo((props) => { label={i18n.translate('xpack.transform.stepDefineForm.dataGridLabel', { defaultMessage: 'Source documents', })} + labelAppend={ + indexPreviewProps.rowCount === MAX_ROW_COUNT && ( + {rowCountInfoLabel} + ) + } > @@ -503,6 +518,11 @@ export const StepDefineForm: FC = React.memo((props) => { label={i18n.translate('xpack.transform.stepDefineForm.previewLabel', { defaultMessage: 'Preview', })} + labelAppend={ + previewProps.rowCount === MAX_ROW_COUNT && ( + {rowCountInfoLabel} + ) + } > <> diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 67c067d26999e..ff644d2f55e1b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5221,7 +5221,6 @@ "kbn-esql-validation-autocomplete.esql.definition.assignDoc": "Affecter (=)", "kbn-esql-validation-autocomplete.esql.definition.divideDoc": "Diviser (/)", "kbn-esql-validation-autocomplete.esql.definition.equalToDoc": "Égal à", - "kbn-esql-validation-autocomplete.esql.definition.functionsDoc": "Afficher les fonctions ES|QL disponibles avec signatures", "kbn-esql-validation-autocomplete.esql.definition.greaterThanDoc": "Supérieur à", "kbn-esql-validation-autocomplete.esql.definition.greaterThanOrEqualToDoc": "Supérieur ou égal à", "kbn-esql-validation-autocomplete.esql.definition.inDoc": "Teste si la valeur d'une expression est contenue dans une liste d'autres expressions", @@ -32025,7 +32024,6 @@ "xpack.observability_onboarding.installElasticAgent.troubleshooting": "Résolution des problèmes", "xpack.observability_onboarding.installIntegration.error.unauthorized": "Le privilège Kibana {requiredKibanaPrivileges} requis est manquant. Veuillez ajouter le privilège requis au rôle de l'utilisateur authentifié.", "xpack.observability_onboarding.installOtelCollector.configStep.copyCommand": "Copier dans le presse-papiers", - "xpack.observability_onboarding.installOtelCollector.configStep.downloadConfigButton": "Télécharger le manifeste", "xpack.observability_onboarding.otelLogs.status.failed": "Échec de l'installation de l'intégration", "xpack.observability_onboarding.otelLogs.status.failedDetails": "Les données entrantes peuvent ne pas être indexées correctement. Détails :", "xpack.observability_onboarding.otelLogsPanel.choosePlatform": "Choisissez une plateforme", @@ -32036,11 +32034,9 @@ "xpack.observability_onboarding.otelLogsPanel.feedbackButtons.title": "Donner un retour", "xpack.observability_onboarding.otelLogsPanel.historicalDataDescription": "Les nouveaux messages de log sont collectés à partir de la configuration.", "xpack.observability_onboarding.otelLogsPanel.historicalDataDescription2": "Le chemin des logs par défaut est /var/log/*. Vous pouvez si nécessaire modifier ce chemin dans le fichier otel.yml.", - "xpack.observability_onboarding.otelLogsPanel.kubernetesApplyCommandPromptLabel": "À partir du répertoire où le manifeste est téléchargé, exécutez la commande suivante pour installer le collecteur sur chaque nœud de votre cluster :", "xpack.observability_onboarding.otelLogsPanel.limitationTitle": "Informations sur la configuration", "xpack.observability_onboarding.otelLogsPanel.p.runTheCommandOnYourHostLabel": "Exécutez la commande suivante sur votre hôte pour télécharger et configurer le collecteur.", "xpack.observability_onboarding.otelLogsPanel.p.startTheCollectorLabel": "Exécutez la commande suivante pour lancer le collecteur", - "xpack.observability_onboarding.otelLogsPanel.steps.downloadManifest": "Télécharger le manifeste :", "xpack.observability_onboarding.otelLogsPanel.steps.platform": "Sélectionnez votre plateforme", "xpack.observability_onboarding.otelLogsPanel.steps.start": "Lancez le collecteur", "xpack.observability_onboarding.otelLogsPanel.techPreviewBadge.label": "Version d'évaluation technique", @@ -47532,4 +47528,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.", "xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes." } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9df94795eb726..088d1e09474bd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5214,7 +5214,6 @@ "kbn-esql-validation-autocomplete.esql.definition.assignDoc": "割り当て(=)", "kbn-esql-validation-autocomplete.esql.definition.divideDoc": "除算(/)", "kbn-esql-validation-autocomplete.esql.definition.equalToDoc": "等しい", - "kbn-esql-validation-autocomplete.esql.definition.functionsDoc": "ES|QLで使用可能な関数と署名を表示", "kbn-esql-validation-autocomplete.esql.definition.greaterThanDoc": "より大きい", "kbn-esql-validation-autocomplete.esql.definition.greaterThanOrEqualToDoc": "よりも大きいまたは等しい", "kbn-esql-validation-autocomplete.esql.definition.inDoc": "ある式が取る値が、他の式のリストに含まれているかどうかをテストします", @@ -31770,7 +31769,6 @@ "xpack.observability_onboarding.installElasticAgent.troubleshooting": "トラブルシューティング", "xpack.observability_onboarding.installIntegration.error.unauthorized": "必要なkibana権限{requiredKibanaPrivileges}がありません。認証されたユーザーのロールに必要な権限を追加してください。", "xpack.observability_onboarding.installOtelCollector.configStep.copyCommand": "クリップボードにコピー", - "xpack.observability_onboarding.installOtelCollector.configStep.downloadConfigButton": "マニフェストのダウンロード", "xpack.observability_onboarding.otelLogs.status.failed": "統合のインストールに失敗しました", "xpack.observability_onboarding.otelLogs.status.failedDetails": "受信データは正しくインデックス化されていない可能性があります。詳細:", "xpack.observability_onboarding.otelLogsPanel.choosePlatform": "プラットフォームを選択", @@ -31781,11 +31779,9 @@ "xpack.observability_onboarding.otelLogsPanel.feedbackButtons.title": "フィードバックを作成する", "xpack.observability_onboarding.otelLogsPanel.historicalDataDescription": "今後、新しいログメッセージはセットアップから収集されます。", "xpack.observability_onboarding.otelLogsPanel.historicalDataDescription2": "デフォルトのログのパスは/var/log/*です。必要に応じて、otel.ymlファイルでこのパスを変更できます。", - "xpack.observability_onboarding.otelLogsPanel.kubernetesApplyCommandPromptLabel": "マニフェストがダウンロードされるディレクトリから、次のコマンドを実行し、クラスターのすべてのノードでコレクターをインストールします。", "xpack.observability_onboarding.otelLogsPanel.limitationTitle": "構成情報", "xpack.observability_onboarding.otelLogsPanel.p.runTheCommandOnYourHostLabel": "ホストで次のコマンドを実行して、コレクターをダウンロード、構成します。", "xpack.observability_onboarding.otelLogsPanel.p.startTheCollectorLabel": "コレクターを開始するには、次のコマンドを実行してください", - "xpack.observability_onboarding.otelLogsPanel.steps.downloadManifest": "マニフェストをダウンロード:", "xpack.observability_onboarding.otelLogsPanel.steps.platform": "プラットフォームを選択", "xpack.observability_onboarding.otelLogsPanel.steps.start": "コレクターを開始", "xpack.observability_onboarding.otelLogsPanel.techPreviewBadge.label": "テクニカルプレビュー", @@ -47270,4 +47266,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3f8eccbd7faf4..0f1ad4f33a1f6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5225,7 +5225,6 @@ "kbn-esql-validation-autocomplete.esql.definition.assignDoc": "分配 (=)", "kbn-esql-validation-autocomplete.esql.definition.divideDoc": "除 (/)", "kbn-esql-validation-autocomplete.esql.definition.equalToDoc": "等于", - "kbn-esql-validation-autocomplete.esql.definition.functionsDoc": "显示带签名的 ES|QL 可用函数", "kbn-esql-validation-autocomplete.esql.definition.greaterThanDoc": "大于", "kbn-esql-validation-autocomplete.esql.definition.greaterThanOrEqualToDoc": "大于或等于", "kbn-esql-validation-autocomplete.esql.definition.inDoc": "测试某表达式接受的值是否包含在其他表达式列表中", @@ -31812,7 +31811,6 @@ "xpack.observability_onboarding.installElasticAgent.troubleshooting": "故障排除", "xpack.observability_onboarding.installIntegration.error.unauthorized": "缺失所需的 Kibana 权限 {requiredKibanaPrivileges},请将所需权限添加到已通过身份验证的用户的角色。", "xpack.observability_onboarding.installOtelCollector.configStep.copyCommand": "复制到剪贴板", - "xpack.observability_onboarding.installOtelCollector.configStep.downloadConfigButton": "下载清单", "xpack.observability_onboarding.otelLogs.status.failed": "集成安装失败", "xpack.observability_onboarding.otelLogs.status.failedDetails": "传入数据可能未正确索引。详情:", "xpack.observability_onboarding.otelLogsPanel.choosePlatform": "选择平台", @@ -31823,11 +31821,9 @@ "xpack.observability_onboarding.otelLogsPanel.feedbackButtons.title": "反馈", "xpack.observability_onboarding.otelLogsPanel.historicalDataDescription": "将从设置完成后收集新的日志消息。", "xpack.observability_onboarding.otelLogsPanel.historicalDataDescription2": "默认日志路径为 /var/log/*。如果需要,可以在 otel.yml 文件中更改此路径。", - "xpack.observability_onboarding.otelLogsPanel.kubernetesApplyCommandPromptLabel": "从下载清单的目录中,运行以下命令以在您集群的每个节点上安装收集器:", "xpack.observability_onboarding.otelLogsPanel.limitationTitle": "配置信息", "xpack.observability_onboarding.otelLogsPanel.p.runTheCommandOnYourHostLabel": "在您的主机上运行以下命令,以下载和配置收集器。", "xpack.observability_onboarding.otelLogsPanel.p.startTheCollectorLabel": "运行以下命令以启动收集器", - "xpack.observability_onboarding.otelLogsPanel.steps.downloadManifest": "下载清单:", "xpack.observability_onboarding.otelLogsPanel.steps.platform": "选择平台", "xpack.observability_onboarding.otelLogsPanel.steps.start": "启动收集器", "xpack.observability_onboarding.otelLogsPanel.techPreviewBadge.label": "技术预览", @@ -47323,4 +47319,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/management/index.js b/x-pack/test/api_integration/apis/management/index.js index d8a9ed76dd2f9..852348d8d243f 100644 --- a/x-pack/test/api_integration/apis/management/index.js +++ b/x-pack/test/api_integration/apis/management/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./index_lifecycle_management')); loadTestFile(require.resolve('./snapshot_restore')); + loadTestFile(require.resolve('./ingest_pipelines')); }); } diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/databases.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/databases.ts index 93a7ccc7d4088..98916ab04509e 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/databases.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/databases.ts @@ -13,17 +13,25 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const ingestPipelines = getService('ingestPipelines'); const url = `/api/ingest_pipelines/databases`; - const databaseName = 'GeoIP2-Anonymous-IP'; - const normalizedDatabaseName = 'geoip2-anonymous-ip'; + const maxmindDatabaseName = 'GeoIP2-Anonymous-IP'; + const normalizedMaxmindDatabaseName = 'geoip2-anonymous-ip'; + const ipinfoDatabaseName = 'asn'; + const normalizedIpinfoDatabaseName = 'asn'; - describe('Manage databases', function () { + // Failing: See https://github.com/elastic/kibana/issues/196765 + // Failing: See https://github.com/elastic/kibana/issues/196765 + describe.skip('Manage databases', function () { after(async () => { await ingestPipelines.api.deleteGeoipDatabases(); }); describe('Create', () => { - it('creates a geoip database when using a correct database name', async () => { - const database = { maxmind: '123456', databaseName }; + it('creates a maxmind geoip database when using a correct database name', async () => { + const database = { + databaseType: 'maxmind', + databaseName: maxmindDatabaseName, + maxmind: '123456', + }; const { body } = await supertest .post(url) .set('kbn-xsrf', 'xxx') @@ -31,13 +39,27 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(body).to.eql({ - name: databaseName, - id: normalizedDatabaseName, + name: maxmindDatabaseName, + id: normalizedMaxmindDatabaseName, }); }); - it('creates a geoip database when using an incorrect database name', async () => { - const database = { maxmind: '123456', databaseName: 'Test' }; + it('creates an ipinfo geoip database when using a correct database name', async () => { + const database = { databaseType: 'ipinfo', databaseName: ipinfoDatabaseName }; + const { body } = await supertest + .post(url) + .set('kbn-xsrf', 'xxx') + .send(database) + .expect(200); + + expect(body).to.eql({ + name: ipinfoDatabaseName, + id: normalizedIpinfoDatabaseName, + }); + }); + + it('returns error when creating a geoip database with an incorrect database name', async () => { + const database = { databaseType: 'maxmind', databaseName: 'Test', maxmind: '123456' }; await supertest.post(url).set('kbn-xsrf', 'xxx').send(database).expect(400); }); }); @@ -47,8 +69,13 @@ export default function ({ getService }: FtrProviderContext) { const { body } = await supertest.get(url).set('kbn-xsrf', 'xxx').expect(200); expect(body).to.eql([ { - id: normalizedDatabaseName, - name: databaseName, + id: normalizedIpinfoDatabaseName, + name: ipinfoDatabaseName, + type: 'ipinfo', + }, + { + id: normalizedMaxmindDatabaseName, + name: maxmindDatabaseName, type: 'maxmind', }, ]); @@ -58,7 +85,12 @@ export default function ({ getService }: FtrProviderContext) { describe('Delete', () => { it('deletes a geoip database', async () => { await supertest - .delete(`${url}/${normalizedDatabaseName}`) + .delete(`${url}/${normalizedMaxmindDatabaseName}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + await supertest + .delete(`${url}/${normalizedIpinfoDatabaseName}`) .set('kbn-xsrf', 'xxx') .expect(200); }); diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index e43c76d42adfa..9cb5a134681a9 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -30,6 +30,9 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi '--xpack.ruleRegistry.write.enabled=true', '--xpack.ruleRegistry.write.enabled=true', '--xpack.ruleRegistry.write.cache.enabled=false', + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'assistantKnowledgeBaseByDefault', + ])}`, '--monitoring_collection.opentelemetry.metrics.prometheus.enabled=true', ], }, diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts index b2dc2abeca67d..336fcf65c830f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts @@ -8,7 +8,7 @@ import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { - describe('Slo - Burn rate rule', () => { + describe('SLO - Burn rate rule', () => { loadTestFile(require.resolve('./burn_rate_rule')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/custom_mappings/custom_synth_mappings.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/custom_mappings/custom_synth_mappings.ts new file mode 100644 index 0000000000000..b94f5e0ca1135 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/custom_mappings/custom_synth_mappings.ts @@ -0,0 +1,110 @@ +/* + * 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 { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; + +export const logsSynthMappings = (dataset: string): MappingTypeMapping => ({ + properties: { + '@timestamp': { + type: 'date', + ignore_malformed: false, + }, + data_stream: { + properties: { + dataset: { + type: 'constant_keyword', + value: dataset, + }, + namespace: { + type: 'constant_keyword', + value: 'default', + }, + type: { + type: 'constant_keyword', + value: 'logs', + }, + }, + }, + event: { + properties: { + dataset: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + host: { + properties: { + name: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, + }, + }, + input: { + properties: { + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + log: { + properties: { + file: { + properties: { + path: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, + }, + }, + }, + }, + message: { + type: 'match_only_text', + }, + network: { + properties: { + bytes: { + type: 'long', + }, + }, + }, + service: { + properties: { + name: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, + }, + }, + test_field: { + type: 'keyword', + ignore_above: 1024, + }, + tls: { + properties: { + established: { + type: 'boolean', + }, + }, + }, + }, +}); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts index 056bde27fc33c..592d4c160dd9f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts @@ -10,6 +10,7 @@ import { log, timerange } from '@kbn/apm-synthtrace-client'; import { SupertestWithRoleScopeType } from '../../../services'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { createBackingIndexNameWithoutVersion, setDataStreamSettings } from './es_utils'; +import { logsSynthMappings } from './custom_mappings/custom_synth_mappings'; const MORE_THAN_1024_CHARS = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; @@ -27,6 +28,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const hostName = 'synth-host'; const dataStreamName = `${type}-${dataset}-${namespace}`; + const customComponentTemplateName = 'logs-synth@mappings'; + async function callApiAs({ roleScopedSupertestWithCookieCredentials, apiParams: { dataStream, degradedField, lastBackingIndex }, @@ -60,6 +63,29 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { describe('gets limit analysis for a given datastream and degraded field', () => { before(async () => { + await synthtrace.createComponentTemplate( + customComponentTemplateName, + logsSynthMappings(dataset) + ); + await esClient.indices.putIndexTemplate({ + name: dataStreamName, + _meta: { + managed: false, + description: 'custom synth template created by synthtrace tool.', + }, + priority: 500, + index_patterns: [dataStreamName], + composed_of: [ + customComponentTemplateName, + 'logs@mappings', + 'logs@settings', + 'ecs@mappings', + ], + allow_auto_create: true, + data_stream: { + hidden: false, + }, + }); await synthtrace.index([ timerange(start, end) .interval('1m') @@ -151,6 +177,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { after(async () => { await synthtrace.clean(); + await esClient.indices.deleteIndexTemplate({ name: dataStreamName }); + await synthtrace.deleteComponentTemplate(customComponentTemplateName); }); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/create_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/create_slo.ts new file mode 100644 index 0000000000000..28cef8c2c566c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/create_slo.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cleanup, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { getSLOSummaryTransformId, getSLOTransformId } from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; +import { TransformHelper, createTransformHelper } from './helpers/transform'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let transformHelper: TransformHelper; + + describe('Create SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + transformHelper = createTransformHelper(getService); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('creates a new slo and transforms', async () => { + const apiResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + expect(apiResponse).property('id'); + const { id } = apiResponse; + + const definitions = await sloApi.findDefinitions(adminRoleAuthc); + expect(definitions.total).eql(1); + expect(definitions.results[0]).eql({ + budgetingMethod: 'occurrences', + updatedAt: definitions.results[0].updatedAt, + createdAt: definitions.results[0].createdAt, + description: 'Fixture for api integration tests', + enabled: true, + groupBy: 'tags', + id, + indicator: { + params: { + filter: 'system.network.name: eth1', + good: 'container.cpu.user.pct < 1', + index: 'kbn-data-forge*', + timestampField: '@timestamp', + total: 'container.cpu.user.pct: *', + }, + type: 'sli.kql.custom', + }, + name: 'Test SLO for api integration', + objective: { + target: 0.99, + }, + revision: 1, + settings: { + frequency: '1m', + syncDelay: '1m', + preventInitialBackfill: false, + }, + tags: ['test'], + timeWindow: { + duration: '7d', + type: 'rolling', + }, + version: 2, + }); + + const rollUpTransformResponse = await transformHelper.assertExist(getSLOTransformId(id, 1)); + expect(rollUpTransformResponse.transforms[0].source.index).eql(['kbn-data-forge*']); + expect(rollUpTransformResponse.transforms[0].dest).eql({ + index: '.slo-observability.sli-v3.3', + pipeline: `.slo-observability.sli.pipeline-${id}-1`, + }); + expect(rollUpTransformResponse.transforms[0].pivot.group_by).eql({ + 'slo.groupings.tags': { terms: { field: 'tags' } }, + '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, + }); + + const summaryTransformResponse = await transformHelper.assertExist( + getSLOSummaryTransformId(id, 1) + ); + expect(summaryTransformResponse.transforms[0].source.index).eql([ + '.slo-observability.sli-v3.3*', + ]); + expect(summaryTransformResponse.transforms[0].dest).eql({ + index: '.slo-observability.summary-v3.3', + pipeline: `.slo-observability.summary.pipeline-${id}-1`, + }); + }); + + describe('groupBy smoke tests', () => { + it('creates instanceId for SLOs with multi groupBy', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: ['system.network.name', 'event.dataset'] }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql( + 'eth1,system.network' + ); + }); + }); + + it('creates instanceId for SLOs with single groupBy', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: 'system.network.name' }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('eth1'); + }); + }); + + it('creates instanceId for SLOs without groupBy ([])', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: [] }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(300 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); + }); + }); + + it('creates instanceId for SLOs without groupBy (["*"])', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: ['*'] }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); + }); + }); + + it('creates instanceId for SLOs without groupBy ("")', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: '' }), + adminRoleAuthc + ); + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); + }); + }); + }); + }); +} + +const getRollupDataEsQuery = (id: string) => ({ + index: '.slo-observability.sli-v3*', + size: 0, + query: { + bool: { + filter: [ + { + term: { + 'slo.id': id, + }, + }, + ], + }, + }, + aggs: { + last_doc: { + top_hits: { + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + _source: { + includes: ['slo.instanceId'], + }, + size: 1, + }, + }, + }, +}); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/delete_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/delete_slo.ts new file mode 100644 index 0000000000000..733d2b6250c29 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/delete_slo.ts @@ -0,0 +1,120 @@ +/* + * 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 { cleanup, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + getSLOSummaryTransformId, + getSLOTransformId, +} from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; +import { TransformHelper, createTransformHelper } from './helpers/transform'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let transformHelper: TransformHelper; + + describe('Delete SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + transformHelper = createTransformHelper(getService); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('deletes SLO and related resources', async () => { + const response = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + expect(response).property('id'); + const id = response.id; + + await sloApi.delete(id, adminRoleAuthc); + + // Expect no definitions exists + const definitions = await sloApi.findDefinitions(adminRoleAuthc); + expect(definitions.total).eql(0); + + await transformHelper.assertNotFound(getSLOTransformId(id, 1)); + await transformHelper.assertNotFound(getSLOSummaryTransformId(id, 1)); + // expect summary and rollup documents to be deleted + await retry.waitForWithTimeout('SLO summary data is deleted', 60 * 1000, async () => { + const sloSummaryResponseAfterDeletion = await esClient.search({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + body: { + query: { + bool: { + filter: [ + { + term: { 'slo.id': id }, + }, + { + term: { isTempDoc: false }, + }, + ], + }, + }, + }, + }); + if (sloSummaryResponseAfterDeletion.hits.hits.length > 0) { + throw new Error('SLO summary data not deleted yet'); + } + return true; + }); + + await retry.waitForWithTimeout('SLO rollup data is deleted', 60 * 1000, async () => { + const sloRollupResponseAfterDeletion = await esClient.search({ + index: SLO_DESTINATION_INDEX_PATTERN, + body: { + query: { + bool: { + filter: [ + { + term: { 'slo.id': id }, + }, + ], + }, + }, + }, + }); + if (sloRollupResponseAfterDeletion.hits.hits.length > 1) { + throw new Error('SLO rollup data not deleted yet'); + } + return true; + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts new file mode 100644 index 0000000000000..1d1be9dc338af --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts @@ -0,0 +1,101 @@ +/* + * 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 { cleanup, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { InternalRequestHeader, RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let internalHeaders: InternalRequestHeader; + + describe('Find SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + internalHeaders = samlAuth.getInternalRequestHeader(); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('searches SLOs', async () => { + const createResponse1 = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + const createResponse2 = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { name: 'something irrelevant foo' }), + adminRoleAuthc + ); + + const sloId1 = createResponse1.id; + const sloId2 = createResponse2.id; + + // search SLOs + await retry.tryForTime(180 * 1000, async () => { + let response = await supertestWithoutAuth + .get(`/api/observability/slos`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders) + .set('elastic-api-version', '1') + .send(); + + expect(response.body.results).length(2); + + response = await supertestWithoutAuth + .get(`/api/observability/slos?kqlQuery=slo.name%3Airrelevant`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders) + .set('elastic-api-version', '1') + .send() + .expect(200); + + expect(response.body.results).length(1); + expect(response.body.results[0].id).eql(sloId2); + + response = await supertestWithoutAuth + .get(`/api/observability/slos?kqlQuery=slo.name%3Aintegration`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders) + .set('elastic-api-version', '1') + .send() + .expect(200); + + expect(response.body.results).length(1); + expect(response.body.results[0].id).eql(sloId1); + + return true; + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/fixtures/slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/fixtures/slo.ts new file mode 100644 index 0000000000000..dfc216760644c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/fixtures/slo.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CreateSLOInput } from '@kbn/slo-schema'; + +export const DEFAULT_SLO: CreateSLOInput = { + name: 'Test SLO for api integration', + description: 'Fixture for api integration tests', + indicator: { + type: 'sli.kql.custom', + params: { + index: 'kbn-data-forge*', + filter: 'system.network.name: eth1', + good: 'container.cpu.user.pct < 1', + total: 'container.cpu.user.pct: *', + timestampField: '@timestamp', + }, + }, + budgetingMethod: 'occurrences', + timeWindow: { + duration: '7d', + type: 'rolling', + }, + objective: { + target: 0.99, + }, + tags: ['test'], + groupBy: 'tags', +}; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/get_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/get_slo.ts new file mode 100644 index 0000000000000..7a27c3b36fb0d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/get_slo.ts @@ -0,0 +1,72 @@ +/* + * 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 { cleanup, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + + describe('Get SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('get SLO by id', async () => { + const createResponse1 = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + await sloApi.create( + Object.assign({}, DEFAULT_SLO, { name: 'something irrelevant foo' }), + adminRoleAuthc + ); + + expect(createResponse1).property('id'); + const sloId1 = createResponse1.id; + + // get the slo by ID + const getSloResponse = await sloApi.get(sloId1, adminRoleAuthc); + // We cannot assert on the summary values itself - it would make the test too flaky + // But we can assert on the existence of these fields at least. + // On top of whatever the SLO definition contains. + expect(getSloResponse).property('summary'); + expect(getSloResponse).property('meta'); + expect(getSloResponse).property('instanceId'); + expect(getSloResponse.budgetingMethod).eql('occurrences'); + expect(getSloResponse.timeWindow).eql({ duration: '7d', type: 'rolling' }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/dataforge.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/dataforge.ts new file mode 100644 index 0000000000000..04da0f81a0643 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/dataforge.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Dataset, PartialConfig } from '@kbn/data-forge/src/types'; + +export const DATA_FORGE_CONFIG: PartialConfig = { + schedule: [ + { + template: 'good', + start: 'now-15m', + end: 'now+5m', + metrics: [ + { name: 'system.cpu.user.pct', method: 'linear', start: 2.5, end: 2.5 }, + { name: 'system.cpu.total.pct', method: 'linear', start: 0.5, end: 0.5 }, + { name: 'system.cpu.total.norm.pct', method: 'linear', start: 0.8, end: 0.8 }, + ], + }, + ], + indexing: { dataset: 'fake_hosts' as Dataset, eventsPerCycle: 1 }, +}; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/transform.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/transform.ts new file mode 100644 index 0000000000000..37b6ff1396c56 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/transform.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export type TransformHelper = ReturnType; + +export function createTransformHelper( + getService: DeploymentAgnosticFtrProviderContext['getService'] +) { + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const samlAuth = getService('samlAuth'); + + return { + assertNotFound: async (transformId: string) => { + const cookieHeader = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin'); + + return await retry.tryWithRetries( + `Wait for transform ${transformId} to be deleted`, + async () => { + await supertestWithoutAuth + .get(`/internal/transform/transforms/${transformId}`) + .set(cookieHeader) + .set(samlAuth.getInternalRequestHeader()) + .set('elastic-api-version', '1') + .send() + .timeout(10000) + .expect(404); + }, + { retryCount: 10, retryDelay: 3000 } + ); + }, + + assertExist: async (transformId: string) => { + return await retry.tryWithRetries( + `Wait for transform ${transformId} to exist`, + async () => { + const cookieHeader = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin'); + + const response = await supertestWithoutAuth + .get(`/internal/transform/transforms/${transformId}`) + .set(cookieHeader) + .set(samlAuth.getInternalRequestHeader()) + .set('elastic-api-version', '1') + .send() + .timeout(10000) + .expect(200); + return response.body; + }, + { retryCount: 10, retryDelay: 3000 } + ); + }, + }; +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts new file mode 100644 index 0000000000000..d47438d163b13 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('SLO', () => { + loadTestFile(require.resolve('./create_slo')); + loadTestFile(require.resolve('./delete_slo')); + loadTestFile(require.resolve('./get_slo')); + loadTestFile(require.resolve('./find_slo')); + loadTestFile(require.resolve('./reset_slo')); + loadTestFile(require.resolve('./update_slo')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/reset_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/reset_slo.ts new file mode 100644 index 0000000000000..c765c4ea55332 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/reset_slo.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cleanup, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { SLO_MODEL_VERSION, getSLOPipelineId } from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + + describe('Reset SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('resets the related resources', async () => { + const createResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + expect(createResponse).property('id'); + const sloId = createResponse.id; + const sloPipelineId = getSLOPipelineId(sloId, 1); + + // Delete the slo rollup ingest pipeline + await retry.tryForTime(60 * 1000, async () => { + await esClient.ingest.deletePipeline({ id: sloPipelineId }); + return true; + }); + + // reset + const resetResponse = await sloApi.reset(sloId, adminRoleAuthc); + expect(resetResponse).property('version', SLO_MODEL_VERSION); + expect(resetResponse).property('revision', 1); + + // assert the pipeline is re-created + await retry.tryForTime(60 * 1000, async () => { + const response = await esClient.ingest.getPipeline({ id: sloPipelineId }); + return !!response[sloPipelineId]; + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/update_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/update_slo.ts new file mode 100644 index 0000000000000..8946f2d613a99 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/update_slo.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 { cleanup, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { getSLOSummaryTransformId, getSLOTransformId } from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; +import { TransformHelper, createTransformHelper } from './helpers/transform'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let transformHelper: TransformHelper; + + describe('Update SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + transformHelper = createTransformHelper(getService); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('updates the definition without a revision bump', async () => { + const createResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + const sloId = createResponse.id; + + const getResponse = await sloApi.get(sloId, adminRoleAuthc); + expect(getResponse).property('revision', 1); + + const updateResponse = await sloApi.update( + { sloId, slo: Object.assign({}, DEFAULT_SLO, { name: 'updated name' }) }, + adminRoleAuthc + ); + expect(updateResponse).property('revision', 1); + expect(updateResponse).property('name', 'updated name'); + + await transformHelper.assertExist(getSLOTransformId(sloId, 1)); + await transformHelper.assertExist(getSLOSummaryTransformId(sloId, 1)); + }); + + it('updates the definition with a revision bump', async () => { + const createResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + const sloId = createResponse.id; + + const getResponse = await sloApi.get(sloId, adminRoleAuthc); + expect(getResponse).property('revision', 1); + + const updateResponse = await sloApi.update( + { sloId, slo: Object.assign({}, DEFAULT_SLO, { objective: { target: 0.63 } }) }, + adminRoleAuthc + ); + expect(updateResponse).property('revision', 2); + expect(updateResponse.objective).eql({ target: 0.63 }); + + await transformHelper.assertNotFound(getSLOTransformId(sloId, 1)); + await transformHelper.assertNotFound(getSLOSummaryTransformId(sloId, 1)); + + await transformHelper.assertExist(getSLOTransformId(sloId, 2)); + await transformHelper.assertExist(getSLOSummaryTransformId(sloId, 2)); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts index f734f0b805d85..e68aad1824c71 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts @@ -16,5 +16,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('../../apis/observability/dataset_quality')); loadTestFile(require.resolve('../../apis/painless_lab')); loadTestFile(require.resolve('../../apis/saved_objects_management')); + loadTestFile(require.resolve('../../apis/observability/slo')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts index cb51d672ab972..a467264698e57 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) // load new oblt deployment-agnostic test here loadTestFile(require.resolve('../../apis/observability/alerting')); loadTestFile(require.resolve('../../apis/observability/dataset_quality')); + loadTestFile(require.resolve('../../apis/observability/slo')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts index 05db3259ddbc6..8ee202b2cf23e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts @@ -5,155 +5,81 @@ * 2.0. */ -import { - fetchHistoricalSummaryParamsSchema, - FetchHistoricalSummaryResponse, -} from '@kbn/slo-schema'; -import * as t from 'io-ts'; import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { CreateSLOInput, FindSLODefinitionsResponse, UpdateSLOInput } from '@kbn/slo-schema'; import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; -interface SloParams { - id?: string; - name: string; - description: string; - indicator: { - type: 'sli.kql.custom'; - params: { - index: string; - good: string; - total: string; - timestampField: string; - }; - }; - timeWindow: { - duration: string; - type: string; - }; - budgetingMethod: string; - objective: { - target: number; - }; - groupBy: string; -} - -type FetchHistoricalSummaryParams = t.OutputOf< - typeof fetchHistoricalSummaryParamsSchema.props.body ->; - -interface SloRequestParams { - id: string; - roleAuthc: RoleCredentials; -} - export function SloApiProvider({ getService }: DeploymentAgnosticFtrProviderContext) { - const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const samlAuth = getService('samlAuth'); - const retry = getService('retry'); - const config = getService('config'); - const retryTimeout = config.get('timeouts.try'); - const requestTimeout = 30 * 1000; return { - async create(slo: SloParams, roleAuthc: RoleCredentials) { + async create(slo: CreateSLOInput, roleAuthc: RoleCredentials) { const { body } = await supertestWithoutAuth .post(`/api/observability/slos`) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(slo); + .send(slo) + .expect(200); return body; }, - async delete({ id, roleAuthc }: SloRequestParams) { - const response = await supertestWithoutAuth - .delete(`/api/observability/slos/${id}`) + async reset(id: string, roleAuthc: RoleCredentials) { + const { body } = await supertestWithoutAuth + .post(`/api/observability/slos/${id}/_reset`) .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()); - return response; + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(200); + + return body; }, - async fetchHistoricalSummary( - params: FetchHistoricalSummaryParams, + async update( + { sloId, slo }: { sloId: string; slo: UpdateSLOInput }, roleAuthc: RoleCredentials - ): Promise { + ) { const { body } = await supertestWithoutAuth - .post(`/internal/observability/slos/_historical_summary`) + .put(`/api/observability/slos/${sloId}`) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(params); + .send(slo) + .expect(200); + return body; }, - async waitForSloToBeDeleted({ id, roleAuthc }: SloRequestParams) { - return await retry.tryForTime(retryTimeout, async () => { - const response = await supertestWithoutAuth - .delete(`/api/observability/slos/${id}`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .timeout(requestTimeout); - if (!response.ok) { - throw new Error(`SLO with id '${id}' was not deleted`); - } - return response; - }); + async delete(id: string, roleAuthc: RoleCredentials) { + return await supertestWithoutAuth + .delete(`/api/observability/slos/${id}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(204); }, - async waitForSloCreated({ id, roleAuthc }: SloRequestParams) { - return await retry.tryForTime(retryTimeout, async () => { - const response = await supertestWithoutAuth - .get(`/api/observability/slos/${id}`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .timeout(requestTimeout); - if (response.body.id === undefined) { - throw new Error(`No SLO with id '${id}' found`); - } - return response.body; - }); - }, + async get(id: string, roleAuthc: RoleCredentials) { + const { body } = await supertestWithoutAuth + .get(`/api/observability/slos/${id}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(200); - async waitForSloSummaryTempIndexToExist(index: string) { - return await retry.tryForTime(retryTimeout, async () => { - const indexExists = await es.indices.exists({ index, allow_no_indices: false }); - if (!indexExists) { - throw new Error(`SLO summary index '${index}' should exist`); - } - return indexExists; - }); + return body; }, - async getSloData({ sloId, indexName }: { sloId: string; indexName: string }) { - const response = await es.search({ - index: indexName, - body: { - query: { - bool: { - filter: [{ term: { 'slo.id': sloId } }], - }, - }, - }, - }); - return response; - }, - async waitForSloData({ id, indexName }: { id: string; indexName: string }) { - return await retry.tryForTime(retryTimeout, async () => { - const response = await es.search({ - index: indexName, - body: { - query: { - bool: { - filter: [{ term: { 'slo.id': id } }], - }, - }, - }, - }); - if (response.hits.hits.length === 0) { - throw new Error(`No hits found at index '${indexName}' for slo id='${id}'`); - } - return response; - }); + async findDefinitions(roleAuthc: RoleCredentials): Promise { + const { body } = await supertestWithoutAuth + .get(`/api/observability/slos/_definitions`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(200); + + return body; }, + async deleteAllSLOs(roleAuthc: RoleCredentials) { const response = await supertestWithoutAuth .get(`/api/observability/slos/_definitions`) diff --git a/x-pack/test/functional/apps/dataset_quality/custom_mappings/custom_synth_mappings.ts b/x-pack/test/functional/apps/dataset_quality/custom_mappings/custom_synth_mappings.ts new file mode 100644 index 0000000000000..4ae4a39cb121d --- /dev/null +++ b/x-pack/test/functional/apps/dataset_quality/custom_mappings/custom_synth_mappings.ts @@ -0,0 +1,110 @@ +/* + * 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 { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; + +export const logsSynthMappings = (dataset: string): MappingTypeMapping => ({ + properties: { + '@timestamp': { + type: 'date', + ignore_malformed: false, + }, + data_stream: { + properties: { + dataset: { + type: 'constant_keyword', + value: 'degraded.dataset.rca', + }, + namespace: { + type: 'constant_keyword', + value: 'default', + }, + type: { + type: 'constant_keyword', + value: 'logs', + }, + }, + }, + event: { + properties: { + dataset: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + host: { + properties: { + name: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, + }, + }, + input: { + properties: { + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + log: { + properties: { + level: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + message: { + type: 'match_only_text', + }, + network: { + properties: { + bytes: { + type: 'long', + }, + }, + }, + service: { + properties: { + name: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, + }, + }, + test_field: { + type: 'keyword', + ignore_above: 1024, + }, + tls: { + properties: { + established: { + type: 'boolean', + }, + }, + }, + trace: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + }, +}); diff --git a/x-pack/test/functional/apps/dataset_quality/degraded_field_flyout.ts b/x-pack/test/functional/apps/dataset_quality/degraded_field_flyout.ts index 517a7f2ad93fc..2506201aa3b85 100644 --- a/x-pack/test/functional/apps/dataset_quality/degraded_field_flyout.ts +++ b/x-pack/test/functional/apps/dataset_quality/degraded_field_flyout.ts @@ -11,12 +11,12 @@ import { generateShortId, log, timerange } from '@kbn/apm-synthtrace-client'; import { DatasetQualityFtrProviderContext } from './config'; import { createDegradedFieldsRecord, - datasetNames, defaultNamespace, getInitialTestLogs, ANOTHER_1024_CHARS, MORE_THAN_1024_CHARS, } from './data'; +import { logsSynthMappings } from './custom_mappings/custom_synth_mappings'; export default function ({ getService, getPageObjects }: DatasetQualityFtrProviderContext) { const PageObjects = getPageObjects([ @@ -27,15 +27,17 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid ]); const testSubjects = getService('testSubjects'); const synthtrace = getService('logSynthtraceEsClient'); + const esClient = getService('es'); const retry = getService('retry'); const to = new Date().toISOString(); - const degradedDatasetName = datasetNames[2]; + const degradedDatasetName = 'synth.degraded'; const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`; - const degradedDatasetWithLimitsName = 'degraded.dataset.rca'; + const degradedDatasetWithLimitsName = 'synth.degraded.rca'; const degradedDatasetWithLimitDataStreamName = `logs-${degradedDatasetWithLimitsName}-${defaultNamespace}`; const serviceName = 'test_service'; const count = 5; + const customComponentTemplateName = 'logs-synth@mappings'; describe('Degraded fields flyout', () => { before(async () => { @@ -114,6 +116,32 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid describe('testing root cause for ignored fields', () => { before(async () => { + // Create custom component template + await synthtrace.createComponentTemplate( + customComponentTemplateName, + logsSynthMappings(degradedDatasetWithLimitsName) + ); + + // Create custom index template + await esClient.indices.putIndexTemplate({ + name: degradedDatasetWithLimitDataStreamName, + _meta: { + managed: false, + description: 'custom synth template created by synthtrace tool.', + }, + priority: 500, + index_patterns: [degradedDatasetWithLimitDataStreamName], + composed_of: [ + customComponentTemplateName, + 'logs@mappings', + 'logs@settings', + 'ecs@mappings', + ], + allow_auto_create: true, + data_stream: { + hidden: false, + }, + }); // Ingest Degraded Logs with 25 fields await synthtrace.index([ timerange(moment(to).subtract(count, 'minute'), moment(to)) @@ -413,6 +441,10 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid after(async () => { await synthtrace.clean(); + await esClient.indices.deleteIndexTemplate({ + name: degradedDatasetWithLimitDataStreamName, + }); + await synthtrace.deleteComponentTemplate(customComponentTemplateName); }); }); }); diff --git a/x-pack/test/functional/apps/ingest_pipelines/manage_processors.ts b/x-pack/test/functional/apps/ingest_pipelines/manage_processors.ts index a4951a2829fd0..ff87ff3c5ecd7 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/manage_processors.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/manage_processors.ts @@ -12,10 +12,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'ingestPipelines', 'savedObjects']); const security = getService('security'); const maxMindDatabaseName = 'GeoIP2-Anonymous-IP'; - const ipInfoDatabaseName = 'ASN'; + const ipInfoDatabaseName = 'Free IP to ASN'; - // TODO: Fix flaky tests - describe.skip('Ingest Pipelines: Manage Processors', function () { + describe('Ingest Pipelines: Manage Processors', function () { this.tags('smoke'); before(async () => { await security.testUser.setRoles(['manage_processors_user']); @@ -36,8 +35,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('Create a MaxMind database', async () => { await pageObjects.ingestPipelines.openCreateDatabaseModal(); await pageObjects.ingestPipelines.fillAddDatabaseForm( - 'MaxMind', - 'GeoIP2 Anonymous IP', + 'maxmind', + maxMindDatabaseName, '123456' ); await pageObjects.ingestPipelines.clickAddDatabaseButton(); @@ -53,9 +52,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(databaseExists).to.be(true); }); - it('Create an IPInfo database', async () => { + it('Create an IPinfo database', async () => { await pageObjects.ingestPipelines.openCreateDatabaseModal(); - await pageObjects.ingestPipelines.fillAddDatabaseForm('IPInfo', ipInfoDatabaseName); + await pageObjects.ingestPipelines.fillAddDatabaseForm('ipinfo', 'asn'); await pageObjects.ingestPipelines.clickAddDatabaseButton(); // Wait for new row to gets displayed @@ -81,7 +80,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { database.includes(ipInfoDatabaseName) ); expect(ipInfoDatabaseRow).to.contain(ipInfoDatabaseName); - expect(ipInfoDatabaseRow).to.contain('IPInfo'); + expect(ipInfoDatabaseRow).to.contain('IPinfo'); }); it('Modal to delete a database', async () => { diff --git a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts index bbdf53b3eda5c..dc9c74dfa07a5 100644 --- a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts @@ -326,7 +326,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.wizard.assertAdvancedQueryEditorSwitchCheckState(false); await transform.testExecution.logTestStep('enables the index preview histogram charts'); - await transform.wizard.enableIndexPreviewHistogramCharts(false); + await transform.wizard.enableIndexPreviewHistogramCharts(true); await transform.testExecution.logTestStep('displays the index preview histogram charts'); await transform.wizard.assertIndexPreviewHistogramCharts( testData.expected.histogramCharts diff --git a/x-pack/test/functional/es_archives/security_solution/entity_store/data.json b/x-pack/test/functional/es_archives/security_solution/entity_store/data.json index 28498e7cb0917..a7804bd132d20 100644 --- a/x-pack/test/functional/es_archives/security_solution/entity_store/data.json +++ b/x-pack/test/functional/es_archives/security_solution/entity_store/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "a4cf452c1e0375c3d4412cb550ad1783358468a3b3b777da4829d72c7d6fb74f", - "index": ".entities.v1.latest.ea_default_user_entity_store", + "index": ".entities.v1.latest.security_user_default", "source": { "event": { "ingested": "2024-09-11T11:26:49.706875Z" @@ -27,7 +27,7 @@ "id": "LBQAgKHGmpup0Kg9nlKmeQ==", "type": "node", "firstSeenTimestamp": "2024-09-11T10:46:00.000Z", - "definitionId": "ea_default_user_entity_store" + "definitionId": "security_user_default" } } } @@ -37,7 +37,7 @@ "type": "doc", "value": { "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", - "index": ".entities.v1.latest.ea_default_host_entity_store", + "index": ".entities.v1.latest.security_host_default", "source": { "event": { "ingested": "2024-09-11T11:26:49.641707Z" @@ -78,7 +78,7 @@ "id": "ZXKm6GEcUJY6NHkMgPPmGQ==", "type": "node", "firstSeenTimestamp": "2024-09-11T10:46:00.000Z", - "definitionId": "ea_default_host_entity_store" + "definitionId": "security_host_default" } } } diff --git a/x-pack/test/functional/page_objects/ingest_pipelines_page.ts b/x-pack/test/functional/page_objects/ingest_pipelines_page.ts index b62d34b114f4b..efbc86128c224 100644 --- a/x-pack/test/functional/page_objects/ingest_pipelines_page.ts +++ b/x-pack/test/functional/page_objects/ingest_pipelines_page.ts @@ -7,7 +7,6 @@ import path from 'path'; import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function IngestPipelinesPageProvider({ getService, getPageObjects }: FtrProviderContext) { @@ -132,21 +131,23 @@ export function IngestPipelinesPageProvider({ getService, getPageObjects }: FtrP }, async fillAddDatabaseForm(databaseType: string, databaseName: string, maxmind?: string) { - await testSubjects.setValue('databaseTypeSelect', databaseType); + await testSubjects.selectValue('databaseTypeSelect', databaseType); - // Wait for the rest of the fields to get displayed - await pageObjects.common.sleep(1000); - expect(await testSubjects.exists('databaseNameSelect')).to.be(true); + await retry.waitFor('Database name field to be displayed', async () => { + return await testSubjects.isDisplayed('databaseNameSelect'); + }); if (maxmind) { await testSubjects.setValue('maxmindField', maxmind); } - await testSubjects.setValue('databaseNameSelect', databaseName); + + await testSubjects.selectValue('databaseNameSelect', databaseName); }, async clickAddDatabaseButton() { - // Wait for button to get enabled - await pageObjects.common.sleep(1000); + await retry.waitFor('Add button to be enabled', async () => { + return await testSubjects.isEnabled('addGeoipDatabaseSubmit'); + }); await testSubjects.click('addGeoipDatabaseSubmit'); }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 723a2a7d2dfa3..bf431e0021053 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -14,6 +14,7 @@ import { getCreateEsqlRulesSchemaMock } from '@kbn/security-solution-plugin/comm import { RuleExecutionStatusEnum } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_monitoring'; import { getMaxSignalsWarning as getMaxAlertsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils'; +import { EXCLUDED_DATA_TIERS_FOR_RULE_EXECUTION } from '@kbn/security-solution-plugin/common/constants'; import { getPreviewAlerts, previewRule, @@ -25,6 +26,7 @@ import { scheduleRuleRun, stopAllManualRuns, waitForBackfillExecuted, + setAdvancedSettings, } from '../../../../utils'; import { deleteAllRules, @@ -1428,6 +1430,12 @@ export default ({ getService }: FtrProviderContext) => { await indexEnhancedDocuments({ documents: [doc1], interval, id }); }); + afterEach(async () => { + await setAdvancedSettings(supertest, { + [EXCLUDED_DATA_TIERS_FOR_RULE_EXECUTION]: [], + }); + }); + it('should not return requests property when not enabled', async () => { const { logs } = await previewRule({ supertest, @@ -1463,6 +1471,35 @@ export default ({ getService }: FtrProviderContext) => { 'POST /ecs_compliant/_search?ignore_unavailable=true' ); }); + it('should not return requests with any data tier filter', async () => { + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + + expect(requests![0].request).not.toContain('data_frozen'); + }); + it('should return requests with included data tiers filters from advanced settings', async () => { + await setAdvancedSettings(supertest, { + [EXCLUDED_DATA_TIERS_FOR_RULE_EXECUTION]: ['data_frozen'], + }); + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + + expect(requests![0].request).toMatch( + /"must_not":\s*\[\s*{\s*"terms":\s*{\s*"_tier":\s*\[\s*"data_frozen"\s*\]/ + ); + }); }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.specific_rules_mode.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.specific_rules_mode.ts index 8c086c46927e7..8da3f96d41d6c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.specific_rules_mode.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/prebuilt_rules/management/trial_license_complete_tier/upgrade_perform_prebuilt_rules.specific_rules_mode.ts @@ -776,7 +776,6 @@ export default ({ getService }: FtrProviderContext): void => { // Create resolved values different from current values const resolvedValues: { [key: string]: unknown } = { - exceptions_list: [], alert_suppression: { group_by: ['test'], duration: { value: 10, unit: 'm' as const }, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts index 5667762ce95c4..2c12400b7f169 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts @@ -24,3 +24,4 @@ export * from './get_stats'; export * from './get_detection_metrics_from_body'; export * from './get_stats_url'; export * from './combine_to_ndjson'; +export * from './set_advanced_settings'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/set_advanced_settings.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/set_advanced_settings.ts new file mode 100644 index 0000000000000..98fe191096253 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/set_advanced_settings.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; + +import type SuperTest from 'supertest'; + +export const setAdvancedSettings = async ( + supertest: SuperTest.Agent, + settings: Record +) => { + return supertest + .post('/internal/kibana/settings') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ changes: settings }) + .expect(200); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts index 9d7bc9989f3af..63a1530744440 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts @@ -7,26 +7,16 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { EntityStoreUtils, elasticAssetCheckerFactory } from '../../utils'; +import { EntityStoreUtils } from '../../utils'; import { dataViewRouteHelpersFactory } from '../../utils/data_view'; export default ({ getService }: FtrProviderContext) => { const api = getService('securitySolutionApi'); const supertest = getService('supertest'); - const { - expectTransformExists, - expectTransformNotFound, - expectEnrichPolicyExists, - expectEnrichPolicyNotFound, - expectComponentTemplateExists, - expectComponentTemplateNotFound, - expectIngestPipelineExists, - expectIngestPipelineNotFound, - } = elasticAssetCheckerFactory(getService); const utils = EntityStoreUtils(getService); - // TODO: unskip once permissions issue is resolved - describe.skip('@ess @serverless @skipInServerlessMKI Entity Store Engine APIs', () => { + // Failing: See https://github.com/elastic/kibana/issues/196526 + describe.skip('@ess @skipInServerlessMKI Entity Store Engine APIs', () => { const dataView = dataViewRouteHelpersFactory(supertest); before(async () => { @@ -45,20 +35,12 @@ export default ({ getService }: FtrProviderContext) => { it('should have installed the expected user resources', async () => { await utils.initEntityEngineForEntityType('user'); - - await expectTransformExists('entities-v1-latest-ea_default_user_entity_store'); - await expectEnrichPolicyExists('entity_store_field_retention_user_default_v1'); - await expectComponentTemplateExists(`ea_default_user_entity_store-latest@platform`); - await expectIngestPipelineExists(`ea_default_user_entity_store-latest@platform`); + await utils.expectEngineAssetsExist('user'); }); it('should have installed the expected host resources', async () => { await utils.initEntityEngineForEntityType('host'); - - await expectTransformExists('entities-v1-latest-ea_default_host_entity_store'); - await expectEnrichPolicyExists('entity_store_field_retention_host_default_v1'); - await expectComponentTemplateExists(`ea_default_host_entity_store-latest@platform`); - await expectIngestPipelineExists(`ea_default_host_entity_store-latest@platform`); + await utils.expectEngineAssetsExist('host'); }); }); @@ -188,10 +170,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-latest-ea_default_host_entity_store'); - await expectEnrichPolicyNotFound('entity_store_field_retention_host_default_v1'); - await expectComponentTemplateNotFound(`ea_default_host_entity_store-latest@platform`); - await expectIngestPipelineNotFound(`ea_default_host_entity_store-latest@platform`); + await utils.expectEngineAssetsDoNotExist('host'); }); it('should delete the user entity engine', async () => { @@ -204,10 +183,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-latest-ea_default_user_entity_store'); - await expectEnrichPolicyNotFound('entity_store_field_retention_user_default_v1'); - await expectComponentTemplateNotFound(`ea_default_user_entity_store-latest@platform`); - await expectIngestPipelineNotFound(`ea_default_user_entity_store-latest@platform`); + await utils.expectEngineAssetsDoNotExist('user'); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts index ee86231fe23d4..481f7aa4056f6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts @@ -9,15 +9,19 @@ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces'; import { EntityStoreUtils } from '../../utils'; +import { dataViewRouteHelpersFactory } from '../../utils/data_view'; + export default ({ getService }: FtrProviderContextWithSpaces) => { const api = getService('securitySolutionApi'); const spaces = getService('spaces'); const namespace = uuidv4().substring(0, 8); - + const supertest = getService('supertest'); const utils = EntityStoreUtils(getService, namespace); - // TODO: unskip once kibana system user has entity index privileges + // Failing: See https://github.com/elastic/kibana/issues/196546 describe.skip('@ess Entity Store Engine APIs in non-default space', () => { + const dataView = dataViewRouteHelpersFactory(supertest, namespace); + before(async () => { await utils.cleanEngines(); await spaces.create({ @@ -25,9 +29,11 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { name: namespace, disabledFeatures: [], }); + await dataView.create('security-solution'); }); after(async () => { + await dataView.delete('security-solution'); await spaces.delete(namespace); }); @@ -38,18 +44,12 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { it('should have installed the expected user resources', async () => { await utils.initEntityEngineForEntityType('user'); - - const expectedTransforms = [`entities-v1-latest-ea_${namespace}_user_entity_store`]; - - await utils.expectTransformsExist(expectedTransforms); + await utils.expectEngineAssetsExist('user'); }); it('should have installed the expected host resources', async () => { await utils.initEntityEngineForEntityType('host'); - - const expectedTransforms = [`entities-v1-latest-ea_${namespace}_host_entity_store`]; - - await utils.expectTransformsExist(expectedTransforms); + await utils.expectEngineAssetsExist('host'); }); }); @@ -79,9 +79,9 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { expect(getResponse.body).to.eql({ status: 'started', type: 'host', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }); }); @@ -98,9 +98,9 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { expect(getResponse.body).to.eql({ status: 'started', type: 'user', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }); }); }); @@ -116,16 +116,16 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { { status: 'started', type: 'host', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }, { status: 'started', type: 'user', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }, ]); }); @@ -200,10 +200,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { ) .expect(200); - await utils.expectTransformNotFound( - `entities-v1-history-ea_${namespace}_host_entity_store` - ); - await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_host_entity_store`); + await utils.expectEngineAssetsDoNotExist('host'); }); it('should delete the user entity engine', async () => { @@ -219,10 +216,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { ) .expect(200); - await utils.expectTransformNotFound( - `entities-v1-history-ea_${namespace}_user_entity_store` - ); - await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_user_entity_store`); + await utils.expectEngineAssetsDoNotExist('user'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts index 69f9c14d06086..9d7af16c79441 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts @@ -11,8 +11,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { const securitySolutionApi = getService('securitySolutionApi'); - // TODO: unskip once permissions issue is resolved - describe.skip('@ess Entity store - Entities list API', () => { + describe('@ess @skipInServerlessMKI Entity store - Entities list API', () => { describe('when the entity store is disable', () => { it("should return response with success status when the index doesn't exist", async () => { const { body } = await securitySolutionApi.listEntities({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts index 4eba56d3a757b..e94f7b7119ddf 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts @@ -12,7 +12,7 @@ export const dataViewRouteHelpersFactory = ( ) => ({ create: (name: string) => { return supertest - .post(`/api/data_views/data_view`) + .post(`/s/${namespace}/api/data_views/data_view`) .set('kbn-xsrf', 'foo') .send({ data_view: { @@ -26,13 +26,13 @@ export const dataViewRouteHelpersFactory = ( }, delete: (name: string) => { return supertest - .delete(`/api/data_views/data_view/${name}-${namespace}`) + .delete(`/s/${namespace}/api/data_views/data_view/${name}-${namespace}`) .set('kbn-xsrf', 'foo') .expect(200); }, updateIndexPattern: (name: string, indexPattern: string) => { return supertest - .post(`/api/data_views/data_view/${name}-${namespace}`) + .post(`/s/${namespace}/api/data_views/data_view/${name}-${namespace}`) .set('kbn-xsrf', 'foo') .send({ data_view: { diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts index 3ac171de1d4fd..24c1434b5e4a5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts @@ -6,16 +6,27 @@ */ import { EntityType } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/common.gen'; - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; +import { elasticAssetCheckerFactory } from './elastic_asset_checker'; export const EntityStoreUtils = ( getService: FtrProviderContext['getService'], - namespace?: string + namespace: string = 'default' ) => { const api = getService('securitySolutionApi'); const es = getService('es'); const log = getService('log'); + const { + expectTransformExists, + expectTransformNotFound, + expectEnrichPolicyExists, + expectEnrichPolicyNotFound, + expectComponentTemplateExists, + expectComponentTemplateNotFound, + expectIngestPipelineExists, + expectIngestPipelineNotFound, + } = elasticAssetCheckerFactory(getService); log.debug(`EntityStoreUtils namespace: ${namespace}`); @@ -37,17 +48,24 @@ export const EntityStoreUtils = ( } }; - const initEntityEngineForEntityType = (entityType: EntityType) => { - log.info(`Initializing engine for entity type ${entityType} in namespace ${namespace}`); - return api - .initEntityEngine( - { - params: { entityType }, - body: {}, - }, - namespace - ) - .expect(200); + const initEntityEngineForEntityType = async (entityType: EntityType) => { + log.info( + `Initializing engine for entity type ${entityType} in namespace ${namespace || 'default'}` + ); + const res = await api.initEntityEngine( + { + params: { entityType }, + body: {}, + }, + namespace + ); + + if (res.status !== 200) { + log.error(`Failed to initialize engine for entity type ${entityType}`); + log.error(JSON.stringify(res.body)); + } + + expect(res.status).to.eql(200); }; const expectTransformStatus = async ( @@ -78,22 +96,25 @@ export const EntityStoreUtils = ( } }; - const expectTransformNotFound = async (transformId: string, attempts: number = 5) => { - return expectTransformStatus(transformId, false); - }; - const expectTransformExists = async (transformId: string) => { - return expectTransformStatus(transformId, true); + const expectEngineAssetsExist = async (entityType: EntityType) => { + await expectTransformExists(`entities-v1-latest-security_${entityType}_${namespace}`); + await expectEnrichPolicyExists(`entity_store_field_retention_${entityType}_${namespace}_v1`); + await expectComponentTemplateExists(`security_${entityType}_${namespace}-latest@platform`); + await expectIngestPipelineExists(`security_${entityType}_${namespace}-latest@platform`); }; - const expectTransformsExist = async (transformIds: string[]) => - Promise.all(transformIds.map((id) => expectTransformExists(id))); + const expectEngineAssetsDoNotExist = async (entityType: EntityType) => { + await expectTransformNotFound(`entities-v1-latest-security_${entityType}_${namespace}`); + await expectEnrichPolicyNotFound(`entity_store_field_retention_${entityType}_${namespace}_v1`); + await expectComponentTemplateNotFound(`security_${entityType}_${namespace}-latest@platform`); + await expectIngestPipelineNotFound(`security_${entityType}_${namespace}-latest@platform`); + }; return { cleanEngines, initEntityEngineForEntityType, expectTransformStatus, - expectTransformNotFound, - expectTransformExists, - expectTransformsExist, + expectEngineAssetsExist, + expectEngineAssetsDoNotExist, }; }; diff --git a/x-pack/test_serverless/README.md b/x-pack/test_serverless/README.md index 3f8b4fe692de0..44f871273aca2 100644 --- a/x-pack/test_serverless/README.md +++ b/x-pack/test_serverless/README.md @@ -207,6 +207,8 @@ describe("my internal APIs test suite", async function() { With custom native roles now enabled for the Security and Search projects on MKI, the FTR supports defining and authenticating with custom roles in both UI functional tests and API integration tests. +To test role management within the Observability project, you can execute the tests using the existing [config.feature_flags.ts](x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts), where this functionality is explicitly enabled. Though the config is not run on MKI, it provides the ability to test custom roles in Kibana CI before the functionality is enabled in MKI. When roles management is enabled on MKI, these tests can be migrated to the regular FTR config and will be run on MKI. + For compatibility with MKI, the role name `customRole` is reserved for use in tests. The test user is automatically assigned to this role, but before logging in via the browser, generating a cookie header, or creating an API key in each test suite, the role’s privileges must be updated. Note: We are still working on a solution to run these tests against MKI. In the meantime, please tag the suite with `skipMKI`. @@ -229,6 +231,9 @@ await samlAuth.setCustomRole({ }); // Then, log in via the browser as a user with the newly defined privileges await pageObjects.svlCommonPage.loginWithCustomRole(); + +// Make sure to delete the custom role in the 'after' hook +await samlAuth.deleteCustomRole(); ``` FTR api_integration test example: @@ -251,8 +256,9 @@ await samlAuth.setCustomRole({ // Then, generate an API key with the newly defined privileges const roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole'); -// Remember to invalidate the API key after use +// Remember to invalidate the API key after use and delete the custom role await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); +await samlAuth.deleteCustomRole(); ``` ### Testing with feature flags diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/custom_mappings/custom_synth_mappings.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/custom_mappings/custom_synth_mappings.ts new file mode 100644 index 0000000000000..4ae4a39cb121d --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/custom_mappings/custom_synth_mappings.ts @@ -0,0 +1,110 @@ +/* + * 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 { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; + +export const logsSynthMappings = (dataset: string): MappingTypeMapping => ({ + properties: { + '@timestamp': { + type: 'date', + ignore_malformed: false, + }, + data_stream: { + properties: { + dataset: { + type: 'constant_keyword', + value: 'degraded.dataset.rca', + }, + namespace: { + type: 'constant_keyword', + value: 'default', + }, + type: { + type: 'constant_keyword', + value: 'logs', + }, + }, + }, + event: { + properties: { + dataset: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + host: { + properties: { + name: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, + }, + }, + input: { + properties: { + type: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + log: { + properties: { + level: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + message: { + type: 'match_only_text', + }, + network: { + properties: { + bytes: { + type: 'long', + }, + }, + }, + service: { + properties: { + name: { + type: 'keyword', + fields: { + text: { + type: 'match_only_text', + }, + }, + }, + }, + }, + test_field: { + type: 'keyword', + ignore_above: 1024, + }, + tls: { + properties: { + established: { + type: 'boolean', + }, + }, + }, + trace: { + properties: { + id: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + }, +}); diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts index 263dc8652ad75..0ca3163e9d879 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts @@ -10,13 +10,13 @@ import moment from 'moment'; import { generateShortId, log, timerange } from '@kbn/apm-synthtrace-client'; import { createDegradedFieldsRecord, - datasetNames, defaultNamespace, getInitialTestLogs, ANOTHER_1024_CHARS, MORE_THAN_1024_CHARS, } from './data'; import { FtrProviderContext } from '../../../ftr_provider_context'; +import { logsSynthMappings } from './custom_mappings/custom_synth_mappings'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ @@ -28,15 +28,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); const testSubjects = getService('testSubjects'); const synthtrace = getService('svlLogsSynthtraceClient'); + const esClient = getService('es'); const retry = getService('retry'); const to = new Date().toISOString(); - const degradedDatasetName = datasetNames[2]; + const degradedDatasetName = 'synth.degraded'; const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`; - const degradedDatasetWithLimitsName = 'degraded.dataset.rca'; + const degradedDatasetWithLimitsName = 'synth.degraded.rca'; const degradedDatasetWithLimitDataStreamName = `logs-${degradedDatasetWithLimitsName}-${defaultNamespace}`; const serviceName = 'test_service'; const count = 5; + const customComponentTemplateName = 'logs-synth@mappings'; describe('Degraded fields flyout', () => { before(async () => { @@ -112,6 +114,32 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('testing root cause for ignored fields', () => { before(async () => { + // Create custom component template + await synthtrace.createComponentTemplate( + customComponentTemplateName, + logsSynthMappings(degradedDatasetWithLimitsName) + ); + + // Create custom index template + await esClient.indices.putIndexTemplate({ + name: degradedDatasetWithLimitDataStreamName, + _meta: { + managed: false, + description: 'custom synth template created by synthtrace tool.', + }, + priority: 500, + index_patterns: [degradedDatasetWithLimitDataStreamName], + composed_of: [ + customComponentTemplateName, + 'logs@mappings', + 'logs@settings', + 'ecs@mappings', + ], + allow_auto_create: true, + data_stream: { + hidden: false, + }, + }); // Ingest Degraded Logs with 25 fields await synthtrace.index([ timerange(moment(to).subtract(count, 'minute'), moment(to)) @@ -411,6 +439,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await synthtrace.clean(); + await esClient.indices.deleteIndexTemplate({ + name: degradedDatasetWithLimitDataStreamName, + }); + await synthtrace.deleteComponentTemplate(customComponentTemplateName); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts b/x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts index 955d839a38d26..1f087233b52e9 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/index.feature_flags.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless observability UI - feature flags', function () { // add tests that require feature flags, defined in config.feature_flags.ts + loadTestFile(require.resolve('./role_management')); loadTestFile(require.resolve('./infra')); loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts')); loadTestFile(require.resolve('../common/platform_security/roles.ts')); diff --git a/x-pack/test_serverless/functional/test_suites/observability/role_management/custom_role_access.ts b/x-pack/test_serverless/functional/test_suites/observability/role_management/custom_role_access.ts new file mode 100644 index 0000000000000..2db9e4c5d3b16 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/role_management/custom_role_access.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { RoleCredentials } from '../../../../shared/services'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['svlCommonPage', 'timePicker', 'common', 'header']); + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + let roleAuthc: RoleCredentials; + + describe('With custom role', function () { + // skipping on MKI while we are working on a solution + this.tags(['skipMKI']); + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.update({ + defaultIndex: 'logstash-*', + }); + await samlAuth.setCustomRole({ + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }); + // login with custom role + await pageObjects.svlCommonPage.loginWithCustomRole(); + await pageObjects.svlCommonPage.assertUserAvatarExists(); + }); + + after(async () => { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + await kibanaServer.savedObjects.cleanStandardList(); + if (roleAuthc) { + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + } + // delete custom role + await samlAuth.deleteCustomRole(); + }); + + it('should have limited navigation menu', async () => { + await pageObjects.svlCommonPage.assertUserAvatarExists(); + // discover navigation link is present + await testSubjects.existOrFail('~nav-item-id-last-used-logs-viewer'); + + // all other links in navigation menu are hidden + await testSubjects.missingOrFail('~nav-item-id-dashboards'); + await testSubjects.missingOrFail('~nav-item-id-observability-overview:alerts'); + await testSubjects.missingOrFail('~nav-item-id-observability-overview:cases'); + await testSubjects.missingOrFail('~nav-item-id-slo'); + await testSubjects.missingOrFail('~nav-item-id-aiops'); + await testSubjects.missingOrFail('~nav-item-id-inventory'); + await testSubjects.missingOrFail('~nav-item-id-apm'); + await testSubjects.missingOrFail('~nav-item-id-metrics'); + await testSubjects.missingOrFail('~nav-item-id-synthetics'); + + // TODO: 'Add data' and 'Project Settings' should be hidden + // await testSubjects.missingOrFail('~nav-item-id-observabilityOnboarding'); + // await testSubjects.missingOrFail('~nav-item-id-project_settings_project_nav'); + }); + + it('should access Discover app', async () => { + await pageObjects.common.navigateToApp('discover'); + await pageObjects.timePicker.setDefaultAbsoluteRange(); + await pageObjects.header.waitUntilLoadingHasFinished(); + expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true); + expect(await testSubjects.exists('discoverQueryHits')).to.be(true); + }); + + it('should access console with API key', async () => { + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole'); + const { body } = await supertestWithoutAuth + .get('/api/console/api_server') + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .set({ 'kbn-xsrf': 'true' }) + .expect(200); + expect(body.es).to.be.ok(); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/role_management/index.ts b/x-pack/test_serverless/functional/test_suites/observability/role_management/index.ts new file mode 100644 index 0000000000000..063f1c8c8cc2c --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/role_management/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Role Management', function () { + loadTestFile(require.resolve('./custom_role_access')); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/custom_role_access.ts b/x-pack/test_serverless/functional/test_suites/search/custom_role_access.ts index 6e28289d4fb00..7a7931d5f6eea 100644 --- a/x-pack/test_serverless/functional/test_suites/search/custom_role_access.ts +++ b/x-pack/test_serverless/functional/test_suites/search/custom_role_access.ts @@ -53,6 +53,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { if (roleAuthc) { await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); } + // delete custom role + await samlAuth.deleteCustomRole(); }); it('should have limited navigation menu', async () => { diff --git a/yarn.lock b/yarn.lock index 65b4f0cd741a7..f41489d0be37d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22446,10 +22446,10 @@ listr2@^3.8.3: through "^2.3.8" wrap-ansi "^7.0.0" -listr2@^8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.4.tgz#486b51cbdb41889108cb7e2c90eeb44519f5a77f" - integrity sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g== +listr2@^8.2.5: + version "8.2.5" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.5.tgz#5c9db996e1afeb05db0448196d3d5f64fec2593d" + integrity sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ== dependencies: cli-truncate "^4.0.0" colorette "^2.0.20"