diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index aeb15233a85ae..62950b16bec65 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -94,7 +94,7 @@ steps: automatic: - exit_status: '-1' limit: 3 - + - command: .buildkite/scripts/steps/functional/security_serverless.sh label: 'Serverless Security Cypress Tests' agents: @@ -102,12 +102,9 @@ steps: depends_on: build timeout_in_minutes: 40 parallelism: 10 - soft_fail: - - exit_status: 10 + soft_fail: true retry: automatic: - - exit_status: '-1' - limit: 3 - exit_status: '*' limit: 1 artifact_paths: @@ -119,17 +116,14 @@ steps: queue: n2-4-spot depends_on: build timeout_in_minutes: 40 - soft_fail: - - exit_status: 10 + soft_fail: true retry: automatic: - - exit_status: '-1' - limit: 3 - exit_status: '*' limit: 1 artifact_paths: - "target/kibana-security-solution/**/*" - + - command: .buildkite/scripts/steps/functional/security_serverless_investigations.sh label: 'Serverless Security Investigations Cypress Tests' agents: @@ -137,12 +131,9 @@ steps: depends_on: build timeout_in_minutes: 40 parallelism: 4 - soft_fail: - - exit_status: 10 + soft_fail: true retry: automatic: - - exit_status: '-1' - limit: 3 - exit_status: '*' limit: 1 artifact_paths: @@ -155,12 +146,9 @@ steps: depends_on: build timeout_in_minutes: 40 parallelism: 2 - soft_fail: - - exit_status: 10 + soft_fail: true retry: automatic: - - exit_status: '-1' - limit: 3 - exit_status: '*' limit: 1 artifact_paths: diff --git a/.buildkite/pipelines/pull_request/security_solution.yml b/.buildkite/pipelines/pull_request/security_solution.yml index 2b73e9482b156..7e06d4f48c9ea 100644 --- a/.buildkite/pipelines/pull_request/security_solution.yml +++ b/.buildkite/pipelines/pull_request/security_solution.yml @@ -11,4 +11,15 @@ steps: - exit_status: '*' limit: 1 artifact_paths: - - "target/kibana-security-solution/**/*" \ No newline at end of file + - "target/kibana-security-solution/**/*" + + - command: .buildkite/scripts/steps/functional/security_solution_burn.sh + label: 'Security Solution Cypress tests, burning changed specs' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 120 + parallelism: 1 + soft_fail: true + artifact_paths: + - "target/kibana-security-solution/**/*" \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 89e4a2c008727..c4ef8cba67037 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -823,12 +823,13 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations /x-pack/test/stack_functional_integration/apps/management/_index_pattern_create.js @elastic/kibana-data-discovery /x-pack/test/upgrade/apps/discover @elastic/kibana-data-discovery -# Vis Editors +# Visualizations /src/plugins/visualize/ @elastic/kibana-visualizations /x-pack/test/functional/apps/lens @elastic/kibana-visualizations /x-pack/test/api_integration/apis/lens/ @elastic/kibana-visualizations /test/functional/apps/visualize/ @elastic/kibana-visualizations /x-pack/test/functional/apps/graph @elastic/kibana-visualizations +/test/api_integraion/apis/event_annotations @elastic/kibana-visualizations # Global Experience @@ -1157,15 +1158,12 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/plugins/stack_connectors/common/sentinelone @elastic/security-defend-workflows ## Security Solution sub teams - Detection Rule Management -/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine /x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine /x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules @elastic/security-detection-rule-management /x-pack/plugins/security_solution/common/api/detection_engine/rule_management @elastic/security-detection-rule-management /x-pack/plugins/security_solution/common/api/detection_engine/rule_monitoring @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/common/detection_engine/fleet_integrations @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/common/detection_engine/prebuilt_rules @elastic/security-detection-rule-management /x-pack/plugins/security_solution/common/detection_engine/rule_management @elastic/security-detection-rule-management -/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring @elastic/security-detection-rule-management /x-pack/test/security_solution_cypress/cypress/e2e/detection_response/prebuilt_rules @elastic/security-detection-rule-management /x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management @elastic/security-detection-rule-management @@ -1196,24 +1194,37 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/plugins/security_solution/server/lib/detection_engine/rule_management @elastic/security-detection-rule-management /x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring @elastic/security-detection-rule-management /x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/utils @elastic/security-detection-rule-management /x-pack/plugins/security_solution/scripts/openapi @elastic/security-detection-rule-management ## Security Solution sub teams - Detection Engine - -/x-pack/plugins/security_solution/common/api/detection_engine @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/alert_tags @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/index_management @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/model/alerts @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/rule_exceptions @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/signals @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/api/detection_engine/signals_migration @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/cti @elastic/security-detection-engine /x-pack/plugins/security_solution/common/field_maps @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/risk_engine @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/common/components/sourcerer @elastic/security-detection-engine /x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists @elastic/security-detection-engine /x-pack/plugins/security_solution/public/detections/pages/alerts @elastic/security-detection-engine /x-pack/plugins/security_solution/public/entity_analytics @elastic/security-detection-engine +/x-pack/plugins/security_solution/public/exceptions @elastic/security-detection-engine /x-pack/plugins/security_solution/server/lib/detection_engine/migrations @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions @elastic/security-detection-engine /x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview @elastic/security-detection-engine /x-pack/plugins/security_solution/server/lib/detection_engine/rule_types @elastic/security-detection-engine /x-pack/plugins/security_solution/server/lib/detection_engine/routes/index @elastic/security-detection-engine /x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals @elastic/security-detection-engine +/x-pack/plugins/security_solution/server/lib/sourcerer @elastic/security-detection-engine /x-pack/test/security_solution_cypress/cypress/e2e/data_sources @elastic/security-detection-engine /x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_creation @elastic/security-detection-engine @@ -1222,26 +1233,12 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/test/security_solution_cypress/cypress/e2e/exceptions @elastic/security-detection-engine /x-pack/test/security_solution_cypress/cypress/e2e/overview @elastic/security-detection-engine -/x-pack/plugins/security_solution/common/detection_engine/rule_exceptions @elastic/security-detection-engine - -/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions_ui @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/common/components/exceptions @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/exceptions @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists @elastic/security-detection-engine -/x-pack/plugins/security_solution/public/common/components/sourcerer @elastic/security-detection-engine - -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_exceptions @elastic/security-detection-engine -/x-pack/plugins/security_solution/server/lib/sourcerer @elastic/security-detection-engine - ## Security Threat Intelligence - Under Security Platform /x-pack/plugins/security_solution/public/common/components/threat_match @elastic/security-detection-engine ## Security Solution cross teams ownership /x-pack/test/security_solution_cypress/cypress/fixtures @elastic/security-detections-response @elastic/security-threat-hunting /x-pack/test/security_solution_cypress/cypress/helpers @elastic/security-detections-response @elastic/security-threat-hunting -/x-pack/test/security_solution_cypress/cypress/e2e/detection_rules @elastic/security-detection-rule-management @elastic/security-detection-engine /x-pack/test/security_solution_cypress/cypress/objects @elastic/security-detections-response @elastic/security-threat-hunting /x-pack/test/security_solution_cypress/cypress/plugins @elastic/security-detections-response @elastic/security-threat-hunting /x-pack/test/security_solution_cypress/cypress/screens/common @elastic/security-detections-response @elastic/security-threat-hunting @@ -1249,13 +1246,13 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/test/security_solution_cypress/cypress/urls @elastic/security-threat-hunting-investigations @elastic/security-detection-engine /x-pack/plugins/security_solution/common/ecs @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/common/test @elastic/security-detection-rule-management @elastic/security-detection-engine +/x-pack/plugins/security_solution/common/test @elastic/security-detections-response @elastic/security-threat-hunting /x-pack/plugins/security_solution/public/common/components/callouts @elastic/security-detections-response /x-pack/plugins/security_solution/public/common/components/hover_actions @elastic/security-threat-hunting-explore @elastic/security-threat-hunting-investigations -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions @elastic/security-detection-engine @elastic/security-detection-rule-management /x-pack/plugins/security_solution/server/routes @elastic/security-detections-response @elastic/security-threat-hunting +/x-pack/plugins/security_solution/server/utils @elastic/security-detections-response @elastic/security-threat-hunting ## Security Solution sub teams - security-defend-workflows /x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows @@ -1283,7 +1280,11 @@ x-pack/plugins/security_solution/server/lib/telemetry/ @elastic/security-data-an ## Security Solution sub teams - security-engineering-productivity x-pack/test/security_solution_cypress/cypress/README.md @elastic/security-engineering-productivity -x-pack/test/security_solution_cypress @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/es_archives @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/cli_config.ts @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/config.ts @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/runner.ts @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/serverless_config.ts @elastic/security-engineering-productivity ## Security Solution sub teams - adaptive-workload-protection x-pack/plugins/security_solution/public/common/components/sessions_viewer @elastic/sec-cloudnative-integrations @@ -1294,7 +1295,7 @@ x-pack/plugins/security_solution/public/threat_intelligence @elastic/protections x-pack/test/threat_intelligence_cypress @elastic/protections-experience # Security Defend Workflows - OSQuery Ownership -/x-pack/plugins/security_solution/common/detection_engine/rule_response_actions @elastic/security-defend-workflows +/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_response_actions @elastic/security-defend-workflows /x-pack/plugins/security_solution/public/detection_engine/rule_response_actions @elastic/security-defend-workflows /x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions @elastic/security-defend-workflows diff --git a/config/serverless.yml b/config/serverless.yml index ac1af15dc673e..20ba56a52cd7e 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -106,3 +106,6 @@ xpack.alerting.rules.run.ruleTypeOverrides: timeout: 1m xpack.alerting.rules.minimumScheduleInterval.enforce: true xpack.actions.run.maxAttempts: 10 + +# Task Manager +xpack.task_manager.allow_reading_invalid_state: false diff --git a/package.json b/package.json index a4a18ab175ad0..310057cadc17f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dashboarding" ], "private": true, - "version": "8.10.0", + "version": "8.11.0", "branch": "main", "types": "./kibana.d.ts", "tsdocMetadata": "./build/tsdoc-metadata.json", @@ -96,7 +96,7 @@ "@elastic/apm-rum-react": "^1.4.4", "@elastic/charts": "59.1.0", "@elastic/datemath": "5.0.3", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch@8.9.0", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.9.1-canary.1", "@elastic/ems-client": "8.4.0", "@elastic/eui": "86.0.0", "@elastic/filesaver": "1.1.2", @@ -1355,7 +1355,7 @@ "@types/source-map-support": "^0.5.3", "@types/stats-lite": "^2.2.0", "@types/styled-components": "^5.1.0", - "@types/supertest": "^2.0.5", + "@types/supertest": "^2.0.12", "@types/tapable": "^1.0.6", "@types/tar": "^6.1.5", "@types/tempy": "^0.2.0", @@ -1537,8 +1537,8 @@ "style-loader": "^1.1.3", "stylelint": "^14.9.1", "stylelint-scss": "^4.3.0", - "superagent": "^3.8.2", - "supertest": "^3.1.0", + "superagent": "^8.1.2", + "supertest": "^6.3.3", "supports-color": "^7.0.0", "svgo": "^2.8.0", "tape": "^5.0.1", diff --git a/packages/core/http/core-http-browser-internal/src/fetch.test.ts b/packages/core/http/core-http-browser-internal/src/fetch.test.ts index cbf6cde1a90c5..b46a34f768b66 100644 --- a/packages/core/http/core-http-browser-internal/src/fetch.test.ts +++ b/packages/core/http/core-http-browser-internal/src/fetch.test.ts @@ -29,6 +29,7 @@ describe('Fetch', () => { const fetchInstance = new Fetch({ basePath: new BasePath(BASE_PATH), kibanaVersion: 'VERSION', + buildNumber: 1234, executionContext: executionContextMock, }); afterEach(() => { @@ -160,6 +161,7 @@ describe('Fetch', () => { expect(fetchMock.lastOptions()!.headers).toMatchObject({ 'content-type': 'application/json', 'kbn-version': 'VERSION', + 'kbn-build-number': '1234', 'x-elastic-internal-origin': 'Kibana', myheader: 'foo', }); @@ -178,6 +180,19 @@ describe('Fetch', () => { `"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-version]"` ); }); + it('should not allow overwriting of kbn-build-number header', async () => { + fetchMock.get('*', {}); + await expect( + fetchInstance.fetch('/my/path', { + headers: { + myHeader: 'foo', + 'kbn-build-number': 4321, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid fetch headers, headers beginning with \\"kbn-\\" are not allowed: [kbn-build-number]"` + ); + }); it('should not allow overwriting of x-elastic-internal-origin header', async () => { fetchMock.get('*', {}); diff --git a/packages/core/http/core-http-browser-internal/src/fetch.ts b/packages/core/http/core-http-browser-internal/src/fetch.ts index 0683824628972..f6aa29c497d43 100644 --- a/packages/core/http/core-http-browser-internal/src/fetch.ts +++ b/packages/core/http/core-http-browser-internal/src/fetch.ts @@ -31,6 +31,7 @@ import { HttpInterceptHaltError } from './http_intercept_halt_error'; interface Params { basePath: IBasePath; kibanaVersion: string; + buildNumber: number; executionContext: ExecutionContextSetup; } @@ -135,6 +136,7 @@ export class Fetch { 'Content-Type': 'application/json', ...options.headers, 'kbn-version': this.params.kibanaVersion, + 'kbn-build-number': this.params.buildNumber, [ELASTIC_HTTP_VERSION_HEADER]: version, [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'Kibana', ...(!isEmpty(context) ? new ExecutionContextContainer(context).toHeader() : {}), diff --git a/packages/core/http/core-http-browser-internal/src/http_service.ts b/packages/core/http/core-http-browser-internal/src/http_service.ts index d2d0913afae94..d097dc7a14c9a 100644 --- a/packages/core/http/core-http-browser-internal/src/http_service.ts +++ b/packages/core/http/core-http-browser-internal/src/http_service.ts @@ -31,13 +31,14 @@ export class HttpService implements CoreService { public setup({ injectedMetadata, fatalErrors, executionContext }: HttpDeps): HttpSetup { const kibanaVersion = injectedMetadata.getKibanaVersion(); + const buildNumber = injectedMetadata.getKibanaBuildNumber(); const basePath = new BasePath( injectedMetadata.getBasePath(), injectedMetadata.getServerBasePath(), injectedMetadata.getPublicBaseUrl() ); - const fetchService = new Fetch({ basePath, kibanaVersion, executionContext }); + const fetchService = new Fetch({ basePath, kibanaVersion, buildNumber, executionContext }); const loadingCount = this.loadingCount.setup({ fatalErrors }); loadingCount.addLoadingCountSource(fetchService.getRequestCount$()); diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 0268757c12d78..23121d9fa5e9d 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -138,6 +138,8 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { connectorsConfluence: `${ENTERPRISE_SEARCH_DOCS}connectors-confluence.html`, connectorsDropbox: `${ENTERPRISE_SEARCH_DOCS}connectors-dropbox.html`, connectorsContentExtraction: `${ENTERPRISE_SEARCH_DOCS}connectors-content-extraction.html`, + connectorsGithub: `${ENTERPRISE_SEARCH_DOCS}connectors-github.html`, + connectorsGmail: `${ENTERPRISE_SEARCH_DOCS}connectors-gmail.html`, connectorsGoogleCloudStorage: `${ENTERPRISE_SEARCH_DOCS}connectors-google-cloud.html`, connectorsGoogleDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-google-drive.html`, connectorsJira: `${ENTERPRISE_SEARCH_DOCS}connectors-jira.html`, @@ -146,12 +148,15 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { connectorsMySQL: `${ENTERPRISE_SEARCH_DOCS}connectors-mysql.html`, connectorsNative: `${ENTERPRISE_SEARCH_DOCS}connectors.html#connectors-native`, connectorsNetworkDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-network-drive.html`, + connectorsOneDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-onedrive.html`, connectorsOracle: `${ENTERPRISE_SEARCH_DOCS}connectors-oracle.html`, connectorsPostgreSQL: `${ENTERPRISE_SEARCH_DOCS}connectors-postgresql.html`, connectorsS3: `${ENTERPRISE_SEARCH_DOCS}connectors-s3.html`, + connectorsSalesforce: `${ENTERPRISE_SEARCH_DOCS}connectors-salesforce.html`, connectorsServiceNow: `${ENTERPRISE_SEARCH_DOCS}connectors-servicenow.html`, connectorsSharepoint: `${ENTERPRISE_SEARCH_DOCS}connectors-sharepoint.html`, connectorsSharepointOnline: `${ENTERPRISE_SEARCH_DOCS}connectors-sharepoint-online.html`, + connectorsSlack: `${ENTERPRISE_SEARCH_DOCS}connectors-slack.html`, connectorsWorkplaceSearch: `${ENTERPRISE_SEARCH_DOCS}workplace-search-connectors.html`, crawlerExtractionRules: `${ENTERPRISE_SEARCH_DOCS}crawler-extraction-rules.html`, crawlerManaging: `${ENTERPRISE_SEARCH_DOCS}crawler-managing.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 339b8c89263a5..6dd1fc6d2cea0 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -122,20 +122,25 @@ export interface DocLinks { readonly connectorsConfluence: string; readonly connectorsContentExtraction: string; readonly connectorsDropbox: string; + readonly connectorsGithub: string; readonly connectorsGoogleCloudStorage: string; readonly connectorsGoogleDrive: string; + readonly connectorsGmail: string; readonly connectorsJira: string; readonly connectorsMicrosoftSQL: string; readonly connectorsMongoDB: string; readonly connectorsMySQL: string; readonly connectorsNative: string; readonly connectorsNetworkDrive: string; + readonly connectorsOneDrive: string; readonly connectorsOracle: string; readonly connectorsPostgreSQL: string; readonly connectorsS3: string; + readonly connectorsSalesforce: string; readonly connectorsServiceNow: string; readonly connectorsSharepoint: string; readonly connectorsSharepointOnline: string; + readonly connectorsSlack: string; readonly connectorsWorkplaceSearch: string; readonly crawlerExtractionRules: string; readonly crawlerManaging: string; diff --git a/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts b/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts index 14f6201cc8283..32300a2e66c96 100644 --- a/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts +++ b/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts @@ -20,6 +20,7 @@ const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE) => ({ enabled: true, false_positives: ['false positive 1', 'false positive 2'], from: 'now-6m', + investigation_fields: ['custom.field1', 'custom.field2'], immutable: false, name: 'Query with a rule id', query: 'user.name: root or user.name: admin', diff --git a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx index 414565851afce..25974eb536d7d 100644 --- a/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx +++ b/src/plugins/controls/public/control_group/component/control_group_sortable_item.tsx @@ -93,6 +93,7 @@ const SortableControlInner = forwardRef< data-test-subj={`control-frame`} data-render-complete="true" className={classNames('controlFrameWrapper', { + 'controlFrameWrapper--grow': grow, 'controlFrameWrapper-isDragging': isDragging, 'controlFrameWrapper-isEditable': isEditable, 'controlFrameWrapper--small': width === 'small', diff --git a/src/plugins/controls/public/control_group/control_group.scss b/src/plugins/controls/public/control_group/control_group.scss index 82bf3a3d1c854..4591d3d6d8708 100644 --- a/src/plugins/controls/public/control_group/control_group.scss +++ b/src/plugins/controls/public/control_group/control_group.scss @@ -74,23 +74,17 @@ $controlMinWidth: $euiSize * 14; } } -.controlFrame__labelToolTip { - max-width: 40%; -} - .controlFrameWrapper { flex-basis: auto; position: relative; - &.controlFrameWrapper-isEditable { - .controlFrame__formControlLayoutLabel { - padding-left: 0; - } + .controlFrame__labelToolTip { + max-width: 40%; } - &:not(.controlFrameWrapper-isEditable) { - .controlFrameFormControlLayout--twoLine .euiFormControlLayout__childrenWrapper { - border-radius: $euiBorderRadius 0 0 $euiBorderRadius; + &-isEditable { + .controlFrame__formControlLayoutLabel { + padding-left: 0; } } @@ -113,10 +107,9 @@ $controlMinWidth: $euiSize * 14; .controlFrame__control { height: 100%; transition: opacity .1s; + background-color: $euiFormBackgroundColor !important; - &.controlFrame--twoLine { - width: 100%; - } + @include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true); } .controlFrame--controlLoading { @@ -130,6 +123,12 @@ $controlMinWidth: $euiSize * 14; &--small { width: $smallControl; min-width: $smallControl; + + &:not(.controlFrameWrapper--grow) { + .controlFrame__labelToolTip { + max-width: 20%; + } + } } &--medium { diff --git a/src/plugins/controls/public/options_list/components/options_list.scss b/src/plugins/controls/public/options_list/components/options_list.scss index 0309437b8c9b3..981aa3982052b 100644 --- a/src/plugins/controls/public/options_list/components/options_list.scss +++ b/src/plugins/controls/public/options_list/components/options_list.scss @@ -1,117 +1,101 @@ -.optionsList__anchorOverride { - display:block; -} - -.optionsList__popoverOverride { +.optionsList--filterGroup { width: 100%; - height: 100%; -} + height: $euiSizeXXL; + background-color: transparent; -.optionsList__actions { - padding: $euiSizeS; - padding-bottom: 0; - border-bottom: $euiBorderThin; - border-color: darken($euiColorLightestShade, 2%); + box-shadow: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: $euiBorderRadius - 1px; + border-bottom-right-radius: $euiBorderRadius - 1px; - .optionsList__actionsRow { - margin: ($euiSizeS / 2) 0 !important; + .optionsList--filterBtn { + border-radius: 0 !important; + height: $euiButtonHeight; - .optionsList__actionBarDivider { - height: $euiSize; - border-right: $euiBorderThin; + &.optionsList--filterBtnPlaceholder { + color: $euiTextSubduedColor; + font-weight: $euiFontWeightRegular; } - } -} -.optionsList__popoverTitle { - display: flex; - align-items: center; - justify-content: space-between; -} + .optionsList__filterInvalid { + color: $euiTextSubduedColor; + text-decoration: line-through; + margin-left: $euiSizeS; + font-weight: $euiFontWeightRegular; + } -.optionsList__filterInvalid { - color: $euiTextSubduedColor; - text-decoration: line-through; - margin-left: $euiSizeS; - font-weight: $euiFontWeightRegular; -} + .optionsList__negateLabel { + font-weight: bold; + font-size: $euiSizeM; + color: $euiColorDanger; + } -.optionsList__existsFilter { - font-style: italic; + .euiFilterButton__text-hasNotification { + flex-grow: 1; + justify-content: space-between; + width: 0; + } + } } -.optionsList__loadMore { - font-style: italic; +.optionsList--sortPopover { + width: $euiSizeXL * 7; } -.optionsList__negateLabel { - font-weight: bold; - font-size: $euiSizeM; - color: $euiColorDanger; +.optionsList__existsFilter { + font-style: italic; } -.optionsList__actionBarFirstBadge { - margin-left: $euiSizeS; +.optionsList__popoverOverride { + @include euiBottomShadowMedium; + filter: none; // overwrite the default popover shadow + transform: translateY(-$euiSizeS) translateX(0); // prevent "slide in" animation on open/close } -.optionsList-control-ignored-selection-title { - padding-left: $euiSizeS; -} +.optionsList__popover { + .optionsList__actions { + padding: $euiSizeS; + padding-bottom: 0; + border-bottom: $euiBorderThin; + border-color: darken($euiColorLightestShade, 2%); -.optionsList__selectionInvalid { - text-decoration: line-through; - color: $euiTextSubduedColor; -} + .optionsList__sortButton { + box-shadow: inset 0 0 0 $euiBorderWidthThin $euiFormBorderColor; + background-color: $euiFormBackgroundColor; + } -.optionsList--filterBtnWrapper { - height: 100%; -} + .optionsList__actionsRow { + margin: ($euiSizeS / 2) 0 !important; -.optionsList--filterBtn { - .euiFilterButton__text-hasNotification { - flex-grow: 1; - justify-content: space-between; - width: 0; - } - &.optionsList--filterBtnPlaceholder { - .euiFilterButton__textShift { - color: $euiTextSubduedColor; + .optionsList__actionBarDivider { + height: $euiSize; + border-right: $euiBorderThin; + } } } -} - -.optionsList--filterGroupSingle { - box-shadow: none; - height: 100%; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-top-right-radius: $euiBorderRadius - 1px; - border-bottom-right-radius: $euiBorderRadius - 1px; -} - -.optionsList--filterGroup { - width: 100%; -} -.optionsList--hiddenEditorForm { - margin-left: $euiSizeXXL + $euiSizeM; -} + .optionsList-control-ignored-selection-title { + padding-left: $euiSizeS; + } -.optionsList--sortPopover { - width: $euiSizeXL * 7; -} + .optionsList__selectionInvalid { + text-decoration: line-through; + color: $euiTextSubduedColor; + } -.optionslist--loadingMoreGroupLabel { - text-align: center; - padding: $euiSizeM; - font-style: italic; - height: $euiSizeXXL !important; -} + .optionslist--loadingMoreGroupLabel { + text-align: center; + padding: $euiSizeM; + font-style: italic; + height: $euiSizeXXL !important; + } -.optionslist--endOfOptionsGroupLabel { - text-align: center; - font-size: $euiSizeM; - height: auto !important; - color: $euiTextSubduedColor; - padding: $euiSizeM; -} + .optionslist--endOfOptionsGroupLabel { + text-align: center; + font-size: $euiSizeM; + height: auto !important; + color: $euiTextSubduedColor; + padding: $euiSizeM; + } +} \ No newline at end of file diff --git a/src/plugins/controls/public/options_list/components/options_list_control.tsx b/src/plugins/controls/public/options_list/components/options_list_control.tsx index 280eed2034bbb..f72bb37c01055 100644 --- a/src/plugins/controls/public/options_list/components/options_list_control.tsx +++ b/src/plugins/controls/public/options_list/components/options_list_control.tsx @@ -157,15 +157,15 @@ export const OptionsListControl = ({ optionsList.dispatch.setPopoverOpen(false)} - anchorClassName="optionsList__anchorOverride" aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)} + panelClassName="optionsList__popoverOverride" >
- - {field?.type !== 'boolean' && !hideActionBar && ( state.componentState.searchString); const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections); + const hideSort = optionsList.select((state) => state.explicitInput.hideSort); const searchTechnique = optionsList.select((state) => state.explicitInput.searchTechnique); const allowExpensiveQueries = optionsList.select( (state) => state.componentState.allowExpensiveQueries ); - const hideSort = optionsList.select((state) => state.explicitInput.hideSort); - return (
- + {!hideSort && ( - + )} diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx index bd71ad3a9f7d2..9eef78edde7db 100644 --- a/src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx +++ b/src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx @@ -9,7 +9,10 @@ import React from 'react'; import { + EuiIconTip, + EuiFlexItem, EuiProgress, + EuiFlexGroup, EuiButtonGroup, EuiPopoverFooter, useEuiPaddingSize, @@ -35,6 +38,9 @@ export const OptionsListPopoverFooter = ({ isLoading }: { isLoading: boolean }) const optionsList = useOptionsList(); const exclude = optionsList.select((state) => state.explicitInput.exclude); + const allowExpensiveQueries = optionsList.select( + (state) => state.componentState.allowExpensiveQueries + ); return ( <> @@ -53,22 +59,39 @@ export const OptionsListPopoverFooter = ({ isLoading }: { isLoading: boolean }) />
)} -
- - optionsList.dispatch.setExclude(optionId === 'optionsList__excludeResults') - } - buttonSize="compressed" - data-test-subj="optionsList__includeExcludeButtonGroup" - /> -
+ + + optionsList.dispatch.setExclude(optionId === 'optionsList__excludeResults') + } + buttonSize="compressed" + data-test-subj="optionsList__includeExcludeButtonGroup" + /> + + {!allowExpensiveQueries && ( + + + + )} + ); diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_sorting_button.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_sorting_button.tsx index ffb066200248d..878238646c44d 100644 --- a/src/plugins/controls/public/options_list/components/options_list_popover_sorting_button.tsx +++ b/src/plugins/controls/public/options_list/components/options_list_popover_sorting_button.tsx @@ -85,7 +85,6 @@ export const OptionsListPopoverSortingButton = ({ }, [optionsList.dispatch] ); - const SortButton = () => ( setIsSortingPopoverOpen(!isSortingPopoverOpen)} - className="euiFilterGroup" // this gives the button a nice border aria-label={OptionsListStrings.popover.getSortPopoverDescription()} > {OptionsListStrings.popover.getSortPopoverTitle()} @@ -118,6 +117,7 @@ export const OptionsListPopoverSortingButton = ({ aria-labelledby="optionsList_sortingOptions" closePopover={() => setIsSortingPopoverOpen(false)} panelClassName={'optionsList--sortPopover'} + anchorClassName={'optionsList__sortButtonPopoverAnchor'} > diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_title.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_title.tsx deleted file mode 100644 index 9801a7bf2474c..0000000000000 --- a/src/plugins/controls/public/options_list/components/options_list_popover_title.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiIconTip } from '@elastic/eui'; - -import { OptionsListStrings } from './options_list_strings'; -import { useOptionsList } from '../embeddable/options_list_embeddable'; - -export const OptionsListPopoverTitle = () => { - const optionsList = useOptionsList(); - - const allowExpensiveQueries = optionsList.select( - (state) => state.componentState.allowExpensiveQueries - ); - const title = optionsList.select((state) => state.explicitInput.title); - - return ( - - - {title} - {!allowExpensiveQueries && ( - - - - )} - - - ); -}; diff --git a/src/plugins/controls/public/range_slider/components/range_slider.scss b/src/plugins/controls/public/range_slider/components/range_slider.scss index abdd460da7286..d3242a7869963 100644 --- a/src/plugins/controls/public/range_slider/components/range_slider.scss +++ b/src/plugins/controls/public/range_slider/components/range_slider.scss @@ -1,52 +1,34 @@ -.rangeSlider__popoverOverride { - height: 100%; - max-width: 100%; - width: 100%; -} - -@include euiBreakpoint('m', 'l', 'xl') { - .rangeSlider__panelOverride { - min-width: $euiSizeXXL * 12; - } -} - -.rangeSlider__anchorOverride { - >div { - height: 100%; - } -} - .rangeSliderAnchor__button { - width: 100%; - height: 100%; - background-color: $euiFormBackgroundColor; - padding: 0; - - .euiFormControlLayout__childrenWrapper { - border-radius: 0 $euiFormControlBorderRadius $euiFormControlBorderRadius 0 !important; + .euiFormControlLayout { + box-shadow: none; + background-color: transparent; + padding: 0 0 2px 0; + + .euiFormControlLayout__childrenWrapper { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: $euiBorderRadius - 1px; + border-bottom-right-radius: $euiBorderRadius - 1px; + } } +} - .euiToolTipAnchor { - width: 100%; +.rangeSliderAnchor__fieldNumber { + font-weight: $euiFontWeightBold; + box-shadow: none; + text-align: center; + background-color: transparent; + + &:invalid { + color: $euiTextSubduedColor; + text-decoration: line-through; + font-weight: $euiFontWeightRegular; + background-image: none; // hide the red bottom border } - .rangeSliderAnchor__fieldNumber { - font-weight: $euiFontWeightBold; - box-shadow: none; - text-align: center; - background-color: unset; - - &:invalid { - color: $euiTextSubduedColor; - text-decoration: line-through; - font-weight: $euiFontWeightRegular; - background-image: none; // hide the red bottom border - } - - &::placeholder { - font-weight: $euiFontWeightRegular; - color: $euiColorMediumShade; - text-decoration: none; - } + &:placeholder-shown, &::placeholder { + font-weight: $euiFontWeightRegular; + color: $euiTextSubduedColor; + text-decoration: none; } } \ No newline at end of file diff --git a/src/plugins/controls/public/range_slider/components/range_slider_button.tsx b/src/plugins/controls/public/range_slider/components/range_slider_button.tsx deleted file mode 100644 index d24f27e25979b..0000000000000 --- a/src/plugins/controls/public/range_slider/components/range_slider_button.tsx +++ /dev/null @@ -1,86 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useCallback } from 'react'; - -import { EuiFieldNumber, EuiFormControlLayoutDelimited } from '@elastic/eui'; - -import './range_slider.scss'; -import { RangeValue } from '../../../common/range_slider/types'; -import { useRangeSlider } from '../embeddable/range_slider_embeddable'; - -export const RangeSliderButton = ({ - value, - onChange, - isPopoverOpen, - setIsPopoverOpen, -}: { - value: RangeValue; - isPopoverOpen: boolean; - setIsPopoverOpen: (open: boolean) => void; - onChange: (newRange: RangeValue) => void; -}) => { - const rangeSlider = useRangeSlider(); - - const min = rangeSlider.select((state) => state.componentState.min); - const max = rangeSlider.select((state) => state.componentState.max); - const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid); - - const id = rangeSlider.select((state) => state.explicitInput.id); - - const isLoading = rangeSlider.select((state) => state.output.loading); - - const onClick = useCallback( - (event) => { - // the popover should remain open if the click/focus target is one of the number inputs - if (isPopoverOpen && event.target instanceof HTMLInputElement) { - return; - } - setIsPopoverOpen(true); - }, - [isPopoverOpen, setIsPopoverOpen] - ); - - return ( - { - onChange([event.target.value, value[1]]); - }} - placeholder={String(min)} - isInvalid={isInvalid} - className={'rangeSliderAnchor__fieldNumber'} - data-test-subj={'rangeSlider__lowerBoundFieldNumber'} - /> - } - endControl={ - { - onChange([value[0], event.target.value]); - }} - placeholder={String(max)} - isInvalid={isInvalid} - className={'rangeSliderAnchor__fieldNumber'} - data-test-subj={'rangeSlider__upperBoundFieldNumber'} - /> - } - /> - ); -}; diff --git a/src/plugins/controls/public/range_slider/components/range_slider_control.tsx b/src/plugins/controls/public/range_slider/components/range_slider_control.tsx index 8a9503ea48a5a..8cd56283467d0 100644 --- a/src/plugins/controls/public/range_slider/components/range_slider_control.tsx +++ b/src/plugins/controls/public/range_slider/components/range_slider_control.tsx @@ -7,27 +7,43 @@ */ import { debounce } from 'lodash'; -import React, { FC, useState, useRef, useMemo, useEffect } from 'react'; +import React, { FC, useState, useMemo, useEffect, useCallback, useRef } from 'react'; -import { EuiInputPopover } from '@elastic/eui'; +import { EuiRangeTick, EuiDualRange, EuiDualRangeProps } from '@elastic/eui'; +import { pluginServices } from '../../services'; +import { RangeValue } from '../../../common/range_slider/types'; import { useRangeSlider } from '../embeddable/range_slider_embeddable'; -import { RangeSliderPopover, EuiDualRangeRef } from './range_slider_popover'; - import { ControlError } from '../../control_group/component/control_error_component'; -import { RangeValue } from '../../../common/range_slider/types'; -import { RangeSliderButton } from './range_slider_button'; + import './range_slider.scss'; export const RangeSliderControl: FC = () => { - const rangeRef = useRef(null); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - + /** Controls Services Context */ + const { + dataViews: { get: getDataViewById }, + } = pluginServices.getServices(); const rangeSlider = useRangeSlider(); + const rangeSliderRef = useRef(null); - const error = rangeSlider.select((state) => state.componentState.error); + // Embeddable explicit input + const id = rangeSlider.select((state) => state.explicitInput.id); const value = rangeSlider.select((state) => state.explicitInput.value); + + // Embeddable cmponent state + const min = rangeSlider.select((state) => state.componentState.min); + const max = rangeSlider.select((state) => state.componentState.max); + const error = rangeSlider.select((state) => state.componentState.error); + const fieldSpec = rangeSlider.select((state) => state.componentState.field); + const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid); + + // Embeddable output + const isLoading = rangeSlider.select((state) => state.output.loading); + const dataViewId = rangeSlider.select((state) => state.output.dataViewId); + + // React component state const [displayedValue, setDisplayedValue] = useState(value ?? ['', '']); + const [fieldFormatter, setFieldFormatter] = useState(() => (toFormat: string) => toFormat); const debouncedOnChange = useMemo( () => @@ -37,45 +53,132 @@ export const RangeSliderControl: FC = () => { [rangeSlider.dispatch] ); + /** + * derive field formatter from fieldSpec and dataViewId + */ useEffect(() => { - debouncedOnChange(displayedValue); - }, [debouncedOnChange, displayedValue]); + (async () => { + if (!dataViewId || !fieldSpec) return; + // dataViews are cached, and should always be available without having to hit ES. + const dataView = await getDataViewById(dataViewId); + setFieldFormatter( + () => + dataView?.getFormatterForField(fieldSpec).getConverterFor('text') ?? + ((toFormat: string) => toFormat) + ); + })(); + }, [fieldSpec, dataViewId, getDataViewById]); + /** + * This will recalculate the displayed min/max of the range slider to allow for selections smaller + * than the `min` and larger than the `max` + */ + const [displayedMin, displayedMax] = useMemo((): [number, number] => { + if (min === undefined || max === undefined) return [-Infinity, Infinity]; + const selectedValue = value ?? ['', '']; + const [selectedMin, selectedMax] = [ + selectedValue[0] === '' ? min : parseFloat(selectedValue[0]), + selectedValue[1] === '' ? max : parseFloat(selectedValue[1]), + ]; + return [Math.min(selectedMin, min), Math.max(selectedMax, max ?? Infinity)]; + }, [min, max, value]); + + /** + * The following `useEffect` ensures that the changes to the value that come from the embeddable (for example, + * from the `reset` button on the dashboard or via chaining) are reflected in the displayed value + */ useEffect(() => { setDisplayedValue(value ?? ['', '']); }, [value]); - const button = ( - + const ticks: EuiRangeTick[] = useMemo(() => { + return [ + { value: min ?? -Infinity, label: fieldFormatter(String(min)) }, + { value: max ?? Infinity, label: fieldFormatter(String(max)) }, + ]; + }, [min, max, fieldFormatter]); + + const levels = useMemo(() => { + return [ + { + min: min ?? -Infinity, + max: max ?? Infinity, + color: 'success', + }, + ]; + }, [min, max]); + + const disablePopover = useMemo( + () => + isLoading || + displayedMin === -Infinity || + displayedMax === Infinity || + displayedMin === displayedMax, + [isLoading, displayedMin, displayedMax] + ); + + const getCommonInputProps = useCallback( + ({ + inputValue, + testSubj, + placeholder, + }: { + inputValue: string; + testSubj: string; + placeholder: string; + }) => { + return { + isInvalid, + placeholder, + readOnly: false, // overwrites `canOpenPopover` to ensure that the inputs are always clickable + className: 'rangeSliderAnchor__fieldNumber', + 'data-test-subj': `rangeSlider__${testSubj}`, + value: inputValue === placeholder ? '' : inputValue, + }; + }, + [isInvalid] ); return error ? ( ) : ( - { - setIsPopoverOpen(false); - }} - anchorPosition="downCenter" - attachToAnchor={false} - disableFocusTrap - onPanelResize={(width) => { - rangeRef.current?.onResize(width); - }} - > - - + + { + // when the pin is dropped (on mouse up), cancel any pending debounced changes and force the change + // in value to happen instantly (which, in turn, will re-calculate the min/max for the slider due to + // the `useEffect` above. + debouncedOnChange.cancel(); + rangeSlider.dispatch.setSelectedRange(displayedValue); + }} + readOnly={disablePopover} + showInput={'inputWithPopover'} + data-test-subj="rangeSlider__slider" + minInputProps={getCommonInputProps({ + inputValue: displayedValue[0], + testSubj: 'lowerBoundFieldNumber', + placeholder: String(min ?? -Infinity), + })} + maxInputProps={getCommonInputProps({ + inputValue: displayedValue[1], + testSubj: 'upperBoundFieldNumber', + placeholder: String(max ?? Infinity), + })} + value={[displayedValue[0] || displayedMin, displayedValue[1] || displayedMax]} + onChange={([minSelection, maxSelection]: [number | string, number | string]) => { + setDisplayedValue([String(minSelection), String(maxSelection)]); + debouncedOnChange([String(minSelection), String(maxSelection)]); + }} + /> + ); }; diff --git a/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx b/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx deleted file mode 100644 index dd9bcffb9ad7f..0000000000000 --- a/src/plugins/controls/public/range_slider/components/range_slider_popover.tsx +++ /dev/null @@ -1,126 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { FC, ComponentProps, Ref, useEffect, useState, useMemo } from 'react'; -import useMount from 'react-use/lib/useMount'; - -import { EuiPopoverTitle, EuiDualRange, EuiText } from '@elastic/eui'; -import type { EuiDualRangeClass } from '@elastic/eui/src/components/form/range/dual_range'; - -import { pluginServices } from '../../services'; -import { RangeSliderStrings } from './range_slider_strings'; -import { RangeValue } from '../../../common/range_slider/types'; -import { useRangeSlider } from '../embeddable/range_slider_embeddable'; - -// Unfortunately, wrapping EuiDualRange in `withEuiTheme` has created this annoying/verbose typing -export type EuiDualRangeRef = EuiDualRangeClass & ComponentProps; - -export const RangeSliderPopover: FC<{ - value: RangeValue; - onChange: (newRange: RangeValue) => void; - rangeRef?: Ref; -}> = ({ onChange, value, rangeRef }) => { - const [fieldFormatter, setFieldFormatter] = useState(() => (toFormat: string) => toFormat); - - // Controls Services Context - const { - dataViews: { get: getDataViewById }, - } = pluginServices.getServices(); - const rangeSlider = useRangeSlider(); - - // Select current state from Redux using multiple selectors to avoid rerenders. - const dataViewId = rangeSlider.select((state) => state.output.dataViewId); - - const id = rangeSlider.select((state) => state.explicitInput.id); - const title = rangeSlider.select((state) => state.explicitInput.title); - - const min = rangeSlider.select((state) => state.componentState.min); - const max = rangeSlider.select((state) => state.componentState.max); - const fieldSpec = rangeSlider.select((state) => state.componentState.field); - const isInvalid = rangeSlider.select((state) => state.componentState.isInvalid); - - // Caches min and max displayed on popover open so the range slider doesn't resize as selections change - const [rangeSliderMin, setRangeSliderMin] = useState(min); - const [rangeSliderMax, setRangeSliderMax] = useState(max); - - useMount(() => { - const [lowerBoundSelection, upperBoundSelection] = [parseFloat(value[0]), parseFloat(value[1])]; - - setRangeSliderMin( - Math.min( - min, - isNaN(lowerBoundSelection) ? Infinity : lowerBoundSelection, - isNaN(upperBoundSelection) ? Infinity : upperBoundSelection - ) - ); - setRangeSliderMax( - Math.max( - max, - isNaN(lowerBoundSelection) ? -Infinity : lowerBoundSelection, - isNaN(upperBoundSelection) ? -Infinity : upperBoundSelection - ) - ); - }); - - // derive field formatter from fieldSpec and dataViewId - useEffect(() => { - (async () => { - if (!dataViewId || !fieldSpec) return; - // dataViews are cached, and should always be available without having to hit ES. - const dataView = await getDataViewById(dataViewId); - setFieldFormatter( - () => - dataView?.getFormatterForField(fieldSpec).getConverterFor('text') ?? - ((toFormat: string) => toFormat) - ); - })(); - }, [fieldSpec, dataViewId, getDataViewById]); - - const ticks = useMemo(() => { - return [ - { value: min, label: fieldFormatter(String(min)) }, - { value: max, label: fieldFormatter(String(max)) }, - ]; - }, [min, max, fieldFormatter]); - - const levels = useMemo(() => { - return [{ min, max, color: 'success' }]; - }, [min, max]); - - return ( -
- {title} - - {min !== -Infinity && max !== Infinity ? ( - { - onChange([String(minSelection), String(maxSelection)]); - }} - value={value} - ticks={ticks} - levels={levels} - showTicks - fullWidth - ref={rangeRef} - data-test-subj="rangeSlider__slider" - /> - ) : isInvalid ? ( - - {RangeSliderStrings.popover.getNoDataHelpText()} - - ) : ( - - {RangeSliderStrings.popover.getNoAvailableDataHelpText()} - - )} -
- ); -}; diff --git a/src/plugins/controls/public/range_slider/components/range_slider_strings.ts b/src/plugins/controls/public/range_slider/components/range_slider_strings.ts index 874a7d2cf5c39..75382684459b7 100644 --- a/src/plugins/controls/public/range_slider/components/range_slider_strings.ts +++ b/src/plugins/controls/public/range_slider/components/range_slider_strings.ts @@ -10,10 +10,6 @@ import { i18n } from '@kbn/i18n'; export const RangeSliderStrings = { popover: { - getNoDataHelpText: () => - i18n.translate('controls.rangeSlider.popover.noDataHelpText', { - defaultMessage: 'Selected range resulted in no data. No filter was applied.', - }), getNoAvailableDataHelpText: () => i18n.translate('controls.rangeSlider.popover.noAvailableDataHelpText', { defaultMessage: 'There is no data to display. Adjust the time range and filters.', diff --git a/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx index e45927862abfc..e466db5806b0a 100644 --- a/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx +++ b/src/plugins/controls/public/range_slider/embeddable/range_slider_embeddable.tsx @@ -255,8 +255,8 @@ export class RangeSliderEmbeddable }); this.dispatch.setMinMax({ - min: `${min ?? '-Infinity'}`, - max: `${max ?? 'Infinity'}`, + min, + max, }); }; diff --git a/src/plugins/controls/public/range_slider/range_slider_reducers.ts b/src/plugins/controls/public/range_slider/range_slider_reducers.ts index c1b5c93c4ce25..35914473cf4a4 100644 --- a/src/plugins/controls/public/range_slider/range_slider_reducers.ts +++ b/src/plugins/controls/public/range_slider/range_slider_reducers.ts @@ -17,8 +17,6 @@ import { RangeValue } from '../../common/range_slider/types'; export const getDefaultComponentState = (): RangeSliderReduxState['componentState'] => ({ isInvalid: false, - min: -Infinity, - max: Infinity, }); export const rangeSliderReducers = { @@ -51,10 +49,10 @@ export const rangeSliderReducers = { }, setMinMax: ( state: WritableDraft, - action: PayloadAction<{ min: string; max: string }> + action: PayloadAction<{ min?: number; max?: number }> ) => { - state.componentState.min = Math.floor(parseFloat(action.payload.min)); - state.componentState.max = Math.ceil(parseFloat(action.payload.max)); + if (action.payload.min !== undefined) state.componentState.min = Math.floor(action.payload.min); + if (action.payload.max !== undefined) state.componentState.max = Math.ceil(action.payload.max); }, publishFilters: ( state: WritableDraft, diff --git a/src/plugins/controls/public/range_slider/types.ts b/src/plugins/controls/public/range_slider/types.ts index 8b283d300e6ae..1d8c2e3945dc5 100644 --- a/src/plugins/controls/public/range_slider/types.ts +++ b/src/plugins/controls/public/range_slider/types.ts @@ -15,8 +15,8 @@ import { ControlOutput } from '../types'; // Component state is only used by public components. export interface RangeSliderComponentState { field?: FieldSpec; - min: number; - max: number; + min?: number; + max?: number; error?: string; isInvalid?: boolean; } diff --git a/src/plugins/controls/public/time_slider/components/index.scss b/src/plugins/controls/public/time_slider/components/index.scss index 264c71af06297..66fb486c970e8 100644 --- a/src/plugins/controls/public/time_slider/components/index.scss +++ b/src/plugins/controls/public/time_slider/components/index.scss @@ -1,18 +1,7 @@ -.timeSlider__anchorOverride { - >div { - height: 100%; - } -} .timeSlider__popoverOverride { width: 100%; max-inline-size: 100% !important; - max-width: 100%; - height: 100%; -} - -.timeSlider__panelOverride { - min-width: $euiSizeXXL * 15; } .timeSlider-playToggle { @@ -20,20 +9,22 @@ } .timeSlider__anchor { - text-decoration: none; width: 100%; - background-color: $euiFormBackgroundColor; + height: 100%; box-shadow: none; - @include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true); overflow: hidden; - height: 100%; - - &:enabled:focus { - background-color: $euiFormBackgroundColor; - } + @include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true); .euiText { - background-color: $euiFormBackgroundColor !important; + background-color: transparent !important; + + &:hover { + text-decoration: underline; + } + + &:not(.euiFormControlLayoutDelimited__delimiter) { + cursor: pointer !important; + } } .timeSlider__anchorText { diff --git a/src/plugins/controls/public/time_slider/components/time_slider.tsx b/src/plugins/controls/public/time_slider/components/time_slider.tsx index a026bf1bd04f5..9bcfc4bc84e49 100644 --- a/src/plugins/controls/public/time_slider/components/time_slider.tsx +++ b/src/plugins/controls/public/time_slider/components/time_slider.tsx @@ -53,7 +53,6 @@ export const TimeSlider: FC = (props: Props) => { return ( = (props: Props) => { isOpen={isOpen} closePopover={() => timeSlider.dispatch.setIsOpen({ isOpen: false })} panelPaddingSize="s" - anchorPosition="downCenter" - disableFocusTrap - attachToAnchor={false} onPanelResize={onPanelResize} > { const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields: deepMockedFields }); -// FLAKY: https://github.com/elastic/kibana/issues/162997 -describe.skip('saved search embeddable', () => { +describe('saved search embeddable', () => { let mountpoint: HTMLDivElement; let servicesMock: jest.Mocked; @@ -322,7 +321,8 @@ describe.skip('saved search embeddable', () => { expect(search).toHaveBeenCalledTimes(1); }); - it('should not reload when the input title doesnt change', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/162997 + it.skip('should not reload when the input title doesnt change', async () => { const search = jest.fn().mockReturnValue(getSearchResponse(1)); const { embeddable } = createEmbeddable({ searchMock: search, customTitle: 'custom title' }); await waitOneTick(); diff --git a/src/plugins/event_annotation/common/content_management/v1/cm_services.ts b/src/plugins/event_annotation/common/content_management/v1/cm_services.ts index 44991124e472b..8da6efc7c7f08 100644 --- a/src/plugins/event_annotation/common/content_management/v1/cm_services.ts +++ b/src/plugins/event_annotation/common/content_management/v1/cm_services.ts @@ -33,7 +33,7 @@ const eventAnnotationGroupAttributesSchema = schema.object( description: schema.maybe(schema.string()), ignoreGlobalFilters: schema.boolean(), annotations: schema.arrayOf(schema.any()), - dataViewSpec: schema.maybe(schema.any()), + dataViewSpec: schema.oneOf([schema.literal(null), schema.object({}, { unknowns: 'allow' })]), }, { unknowns: 'forbid' } ); diff --git a/src/plugins/event_annotation/common/content_management/v1/types.ts b/src/plugins/event_annotation/common/content_management/v1/types.ts index e12a0bc813174..67ba080a78151 100644 --- a/src/plugins/event_annotation/common/content_management/v1/types.ts +++ b/src/plugins/event_annotation/common/content_management/v1/types.ts @@ -34,7 +34,8 @@ export interface EventAnnotationGroupSavedObjectAttributes { description: string; ignoreGlobalFilters: boolean; annotations: EventAnnotationConfig[]; - dataViewSpec?: DataViewSpec; + // NULL is important here - undefined will not properly remove this property from the saved object + dataViewSpec: DataViewSpec | null; } export interface EventAnnotationGroupSavedObject { diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index c122102ae632a..7a0f00e7a4de2 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -37,6 +37,7 @@ const annotationGroupResolveMocks: Record = tags: [], ignoreGlobalFilters: false, annotations: [], + dataViewSpec: null, }, type: 'event-annotation-group', references: [ @@ -574,7 +575,7 @@ describe('Event Annotation Service', () => { title: 'newGroupTitle', description: 'my description', ignoreGlobalFilters: false, - dataViewSpec: undefined, + dataViewSpec: null, annotations, }, options: { @@ -624,7 +625,7 @@ describe('Event Annotation Service', () => { title: 'newTitle', description: '', annotations: [], - dataViewSpec: undefined, + dataViewSpec: null, ignoreGlobalFilters: false, } as EventAnnotationGroupSavedObjectAttributes, options: { diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index b45748ed06eaf..4003ea524b291 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -218,7 +218,7 @@ export function getEventAnnotationService( description, ignoreGlobalFilters, annotations, - dataViewSpec: dataViewSpec || undefined, + dataViewSpec, }, references, }; diff --git a/test/api_integration/apis/event_annotations/event_annotations.ts b/test/api_integration/apis/event_annotations/event_annotations.ts new file mode 100644 index 0000000000000..01119446d8aa1 --- /dev/null +++ b/test/api_integration/apis/event_annotations/event_annotations.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const CONTENT_ENDPOINT = '/api/content_management/rpc'; + +const CONTENT_TYPE_ID = 'event-annotation-group'; + +const API_VERSION = 1; + +const EXISTING_ID_1 = 'fcebef20-3ba4-11ee-85d3-3dd00bdd66ef'; // from loaded archive +const EXISTING_ID_2 = '0d1aa670-3baf-11ee-a4a7-c11cb33a9549'; // from loaded archive + +const DEFAULT_EVENT_ANNOTATION_GROUP = { + title: 'a group', + description: '', + ignoreGlobalFilters: true, + dataViewSpec: null, + annotations: [ + { + label: 'Event', + type: 'manual', + key: { + type: 'point_in_time', + timestamp: '2023-08-10T15:00:00.000Z', + }, + icon: 'triangle', + id: '499ee351-f541-46e0-b327-b3dcae91aff5', + }, + ], +}; + +const DEFAULT_REFERENCES = [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247', + }, +]; + +export default function ({ getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const supertest = getService('supertest'); + + describe('group API', () => { + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json' + ); + }); + + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json' + ); + }); + + describe('search', () => { + // TODO test tag searching, ordering, pagination, etc + + it(`should retrieve existing groups`, async () => { + const resp = await supertest + .post(`${CONTENT_ENDPOINT}/search`) + .set('kbn-xsrf', 'kibana') + .send({ + contentTypeId: CONTENT_TYPE_ID, + query: { + limit: 1000, + tags: { + included: [], + excluded: [], + }, + }, + version: API_VERSION, + }) + .expect(200); + + const results = resp.body.result.result.hits; + expect(results.length).to.be(2); + expect(results.map(({ id }: { id: string }) => id)).to.eql([EXISTING_ID_2, EXISTING_ID_1]); + }); + }); + + describe('create', () => { + it(`should require dataViewSpec to be specified`, async () => { + const createWithDataViewSpec = (dataViewSpec: any) => + supertest + .post(`${CONTENT_ENDPOINT}/create`) + .set('kbn-xsrf', 'kibana') + .send({ + contentTypeId: CONTENT_TYPE_ID, + data: { ...DEFAULT_EVENT_ANNOTATION_GROUP, dataViewSpec }, + options: { + references: DEFAULT_REFERENCES, + }, + version: API_VERSION, + }); + + const errorResp = await createWithDataViewSpec(undefined).expect(400); + + expect(errorResp.body.message).to.be( + 'Invalid data. [dataViewSpec]: expected at least one defined value but got [undefined]' + ); + + await createWithDataViewSpec(null).expect(200); + + await createWithDataViewSpec({ + someDataViewProp: 'some-value', + }).expect(200); + }); + }); + + describe('update', () => { + it(`should require dataViewSpec to be specified`, async () => { + const updateWithDataViewSpec = (dataViewSpec: any) => + supertest + .post(`${CONTENT_ENDPOINT}/update`) + .set('kbn-xsrf', 'kibana') + .send({ + contentTypeId: CONTENT_TYPE_ID, + data: { ...DEFAULT_EVENT_ANNOTATION_GROUP, dataViewSpec }, + id: EXISTING_ID_1, + options: { + references: DEFAULT_REFERENCES, + }, + version: API_VERSION, + }); + + const errorResp = await updateWithDataViewSpec(undefined).expect(400); + + expect(errorResp.body.message).to.be( + 'Invalid data. [dataViewSpec]: expected at least one defined value but got [undefined]' + ); + + await updateWithDataViewSpec(null).expect(200); + + await updateWithDataViewSpec({ + someDataViewProp: 'some-value', + }).expect(200); + }); + }); + + // TODO - delete + }); +} diff --git a/test/api_integration/apis/event_annotations/index.ts b/test/api_integration/apis/event_annotations/index.ts new file mode 100644 index 0000000000000..91bcdce64443d --- /dev/null +++ b/test/api_integration/apis/event_annotations/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('event annotations', () => { + loadTestFile(require.resolve('./event_annotations')); + }); +} diff --git a/test/api_integration/apis/index.ts b/test/api_integration/apis/index.ts index 241af3b9a6374..89d97bba330ae 100644 --- a/test/api_integration/apis/index.ts +++ b/test/api_integration/apis/index.ts @@ -17,6 +17,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./home')); loadTestFile(require.resolve('./data_view_field_editor')); loadTestFile(require.resolve('./data_views')); + loadTestFile(require.resolve('./event_annotations')); loadTestFile(require.resolve('./kql_telemetry')); loadTestFile(require.resolve('./saved_objects_management')); loadTestFile(require.resolve('./saved_objects')); diff --git a/test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json b/test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json new file mode 100644 index 0000000000000..ed574c749518c --- /dev/null +++ b/test/api_integration/fixtures/kbn_archiver/event_annotations/event_annotations.json @@ -0,0 +1,95 @@ +{ + "attributes": { + "fieldFormatMap": "{\"hour_of_day\":{}}", + "name": "Kibana Sample Data Logs", + "runtimeFieldMap": "{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}", + "timeFieldName": "timestamp", + "title": "kibana_sample_data_logs" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-08-15T19:49:25.494Z", + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "managed": false, + "references": [], + "type": "index-pattern", + "typeMigrationVersion": "8.0.0", + "updated_at": "2023-08-15T19:49:25.494Z", + "version": "WzIyLDFd" +} + +{ + "attributes": { + "annotations": [ + { + "color": "#6092c0", + "filter": { + "language": "kuery", + "query": "agent.keyword : \"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\" ", + "type": "kibana_query" + }, + "icon": "asterisk", + "id": "499ee351-f541-46e0-b327-b3dcae91aff5", + "key": { + "type": "point_in_time" + }, + "label": "Event", + "lineStyle": "dashed", + "timeField": "timestamp", + "type": "query" + } + ], + "dataViewSpec": null, + "description": "", + "ignoreGlobalFilters": true, + "title": "Another group" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-08-15T21:02:32.023Z", + "id": "0d1aa670-3baf-11ee-a4a7-c11cb33a9549", + "managed": false, + "references": [ + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247", + "type": "index-pattern" + } + ], + "type": "event-annotation-group", + "updated_at": "2023-08-15T22:15:43.724Z", + "version": "WzU4LDFd" +} + +{ + "attributes": { + "annotations": [ + { + "icon": "triangle", + "id": "1d9627a8-11dc-44f1-badb-4d40a80b6bee", + "key": { + "timestamp": "2023-08-10T15:00:00.000Z", + "type": "point_in_time" + }, + "label": "Event", + "type": "manual" + } + ], + "dataViewSpec": null, + "description": "", + "ignoreGlobalFilters": true, + "title": "A group" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-08-15T19:50:29.907Z", + "id": "fcebef20-3ba4-11ee-85d3-3dd00bdd66ef", + "managed": false, + "references": [ + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247", + "type": "index-pattern" + } + ], + "type": "event-annotation-group", + "updated_at": "2023-08-15T22:13:19.290Z", + "version": "WzU0LDFd" +} \ No newline at end of file diff --git a/test/functional/apps/dashboard_elements/controls/common/range_slider.ts b/test/functional/apps/dashboard_elements/controls/common/range_slider.ts index 9140c23573c3f..21bdb4f897603 100644 --- a/test/functional/apps/dashboard_elements/controls/common/range_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/common/range_slider.ts @@ -218,7 +218,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.rangeSliderSetUpperBound(firstId, '400'); }); - it('hides range slider in popover when no data available', async () => { + it('cannot open popover when no data available', async () => { await dashboardControls.createControl({ controlType: RANGE_SLIDER_CONTROL, dataViewTitle: 'logstash-*', @@ -226,10 +226,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { width: 'small', }); const secondId = (await dashboardControls.getAllControlIds())[1]; - await dashboardControls.rangeSliderOpenPopover(secondId); - await dashboardControls.rangeSliderPopoverAssertOpen(); + await testSubjects.click( + `range-slider-control-${secondId} > rangeSlider__lowerBoundFieldNumber` + ); // try to open popover await testSubjects.missingOrFail('rangeSlider__slider'); - expect((await testSubjects.getVisibleText('rangeSlider__helpText')).length).to.be.above(0); }); }); diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts index bc5e44cf61c02..720ff928b5605 100644 --- a/test/functional/page_objects/dashboard_page_controls.ts +++ b/test/functional/page_objects/dashboard_page_controls.ts @@ -668,37 +668,47 @@ export class DashboardPageControls extends FtrService { public async rangeSliderSetLowerBound(controlId: string, value: string) { this.log.debug(`Setting range slider lower bound to ${value}`); - await this.testSubjects.setValue( - `range-slider-control-${controlId} > rangeSlider__lowerBoundFieldNumber`, - value - ); + await this.retry.try(async () => { + await this.testSubjects.setValue( + `range-slider-control-${controlId} > rangeSlider__lowerBoundFieldNumber`, + value + ); + expect(await this.rangeSliderGetLowerBoundAttribute(controlId, 'value')).to.be(value); + }); } public async rangeSliderSetUpperBound(controlId: string, value: string) { this.log.debug(`Setting range slider lower bound to ${value}`); - await this.testSubjects.setValue( - `range-slider-control-${controlId} > rangeSlider__upperBoundFieldNumber`, - value - ); + await this.retry.try(async () => { + await this.testSubjects.setValue( + `range-slider-control-${controlId} > rangeSlider__upperBoundFieldNumber`, + value + ); + expect(await this.rangeSliderGetUpperBoundAttribute(controlId, 'value')).to.be(value); + }); } public async rangeSliderOpenPopover(controlId: string) { this.log.debug(`Opening popover for Range Slider: ${controlId}`); - await this.testSubjects.click(`range-slider-control-${controlId}`); + // EuiDualRange only opens the popover when one of the number **inputs** is clicked - it does not open when + // the delimiter is clicked, so need to ensure we're clicking an input + await this.testSubjects.click( + `range-slider-control-${controlId} > rangeSlider__lowerBoundFieldNumber` + ); await this.retry.try(async () => { - await this.testSubjects.existOrFail(`rangeSlider__popover`); + await this.testSubjects.existOrFail(`rangeSlider__slider`); }); } public async rangeSliderEnsurePopoverIsClosed(controlId: string) { - this.log.debug(`Opening popover for Range Slider: ${controlId}`); + this.log.debug(`Closing popover for Range Slider: ${controlId}`); const controlLabel = await this.find.byXPath(`//div[@data-control-id='${controlId}']//label`); await controlLabel.click(); - await this.testSubjects.waitForDeleted(`rangeSlider__popover`); + await this.testSubjects.waitForDeleted(`rangeSlider__slider`); } public async rangeSliderPopoverAssertOpen() { await this.retry.try(async () => { - if (!(await this.testSubjects.exists(`rangeSlider__popover`))) { + if (!(await this.testSubjects.exists(`rangeSlider__slider`))) { throw new Error('range slider popover must be open before calling selectOption'); } }); diff --git a/test/plugin_functional/plugins/core_http/server/plugin.ts b/test/plugin_functional/plugins/core_http/server/plugin.ts index f767f96c44408..0f1d8915825bc 100644 --- a/test/plugin_functional/plugins/core_http/server/plugin.ts +++ b/test/plugin_functional/plugins/core_http/server/plugin.ts @@ -22,6 +22,15 @@ export class CoreHttpPlugin implements Plugin { return res.ok(); } ); + router.get( + { + path: '/api/core_http/headers', + validate: false, + }, + async (ctx, req, res) => { + return res.ok({ body: req.headers }); + } + ); } public start() {} diff --git a/test/plugin_functional/test_suites/core_plugins/http.ts b/test/plugin_functional/test_suites/core_plugins/http.ts index 78682da70e608..6f67daa11e335 100644 --- a/test/plugin_functional/test_suites/core_plugins/http.ts +++ b/test/plugin_functional/test_suites/core_plugins/http.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import SemVer from 'semver'; import { PluginFunctionalProviderContext } from '../../services'; export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { @@ -29,5 +30,22 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const canceledErrorName = await getCancelationErrorName(); expect(canceledErrorName).to.eql('AbortError'); }); + + it('sets the expected headers', async () => { + const headers = await browser.executeAsync>(async (cb) => { + cb(await window._coreProvider.setup.core.http.get('/api/core_http/headers')); + }); + expect(headers).to.have.property('kbn-version'); + expect(!!SemVer.valid(headers['kbn-version'])).to.be(true); + + expect(headers).to.have.property('kbn-build-number'); + expect(headers['kbn-build-number']).to.match(/^\d+$/); + + expect(headers).to.have.property('x-elastic-internal-origin'); + expect(headers['x-elastic-internal-origin']).to.be.a('string'); + + expect(headers).to.have.property('x-kbn-context'); + expect(headers['x-kbn-context']).to.be.a('string'); + }); }); } diff --git a/versions.json b/versions.json index efcd549e2826c..1ef569fcd6a89 100644 --- a/versions.json +++ b/versions.json @@ -2,16 +2,22 @@ "notice": "This file is not maintained outside of the main branch and should only be used for tooling.", "versions": [ { - "version": "8.10.0", + "version": "8.11.0", "branch": "main", "currentMajor": true, "currentMinor": true }, + { + "version": "8.10.0", + "branch": "8.10", + "currentMajor": true, + "previousMinor": true + }, { "version": "8.9.1", "branch": "8.9", "currentMajor": true, - "previousMinor": true + "previousMinor": false }, { "version": "7.17.13", diff --git a/x-pack/package.json b/x-pack/package.json index 8de6e27b8e02b..8dc314122d957 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -1,6 +1,6 @@ { "name": "x-pack", - "version": "8.10.0", + "version": "8.11.0", "author": "Elastic", "private": true, "license": "Elastic-License", diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index eb05133c70835..773b46bdb2ab2 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -80,7 +80,11 @@ export const AssistantHeader: React.FC = ({ justifyContent={'spaceBetween'} > - + { it('the component renders correctly with valid props', () => { - const { getByText, container } = render(); + const { getByText, container } = render( + + + + ); expect(getByText('Test Title')).toBeInTheDocument(); expect(container.querySelector('[data-euiicon-type="globe"]')).not.toBeNull(); }); it('clicking on the popover button opens the popover with the correct link', () => { - const { getByTestId, queryByTestId } = render(, { - wrapper: TestProviders, - }); + const { getByTestId, queryByTestId } = render( + + + , + { + wrapper: TestProviders, + } + ); expect(queryByTestId('tooltipContent')).not.toBeInTheDocument(); fireEvent.click(getByTestId('tooltipIcon')); expect(getByTestId('tooltipContent')).toBeInTheDocument(); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx index 719a02aaee132..f2d77c9fb6716 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx @@ -15,10 +15,14 @@ import { EuiModalHeaderTitle, EuiPopover, EuiText, + EuiTitle, } from '@elastic/eui'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import * as i18n from '../translations'; +import type { Conversation } from '../../..'; +import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; /** * Renders a header title with an icon, a tooltip button, and a popover with @@ -28,7 +32,10 @@ export const AssistantTitle: React.FC<{ title: string | JSX.Element; titleIcon: string; docLinks: Omit; -}> = ({ title, titleIcon, docLinks }) => { + selectedConversation: Conversation | undefined; +}> = ({ title, titleIcon, docLinks, selectedConversation }) => { + const selectedConnectorId = selectedConversation?.apiConfig?.connectorId; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const url = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/security-assistant.html`; @@ -66,32 +73,57 @@ export const AssistantTitle: React.FC<{ return ( - - + + - {title} - - - } - isOpen={isPopoverOpen} - closePopover={closePopover} - anchorPosition="upCenter" - > - -

{i18n.TOOLTIP_TITLE}

-

{content}

-
-
-
+ + + + + +

{title}

+
+
+ + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + anchorPosition="rightUp" + > + +

{i18n.TOOLTIP_TITLE}

+ +

{content}

+
+
+
+
+
+
+ + {}} + onConnectorSelectionChange={() => {}} + selectedConnectorId={selectedConnectorId} + selectedConversation={selectedConversation} + /> + +
); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx index 1976586f64b7a..255cb59be0c20 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/select_system_prompt/index.tsx @@ -91,7 +91,7 @@ const SelectSystemPromptComponent: React.FC = ({ dropdownDisplay: ( - + {i18n.ADD_NEW_SYSTEM_PROMPT} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx index 671cac8ea01c6..1c56e005892c5 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx @@ -46,7 +46,7 @@ export const ConnectorMissingCallout: React.FC = React.memo(

{' '} = React.memo( dropdownDisplay: ( - + {i18n.ADD_NEW_CONNECTOR} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.test.tsx new file mode 100644 index 0000000000000..774098eba8b2e --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.test.tsx @@ -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 React from 'react'; +import { render } from '@testing-library/react'; + +import { noop } from 'lodash/fp'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { ConnectorSelectorInline } from './connector_selector_inline'; +import * as i18n from '../translations'; +import { Conversation } from '../../..'; +import { useLoadConnectors } from '../use_load_connectors'; + +jest.mock('@kbn/triggers-actions-ui-plugin/public/common/constants', () => ({ + loadActionTypes: jest.fn(() => { + return Promise.resolve([ + { + id: '.gen-ai', + name: 'Gen AI', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]); + }), +})); + +jest.mock('../use_load_connectors', () => ({ + useLoadConnectors: jest.fn(() => { + return { + data: [], + error: null, + isSuccess: true, + }; + }), +})); + +const mockConnectors = [ + { + id: 'connectorId', + name: 'Captain Connector', + isMissingSecrets: false, + actionTypeId: '.gen-ai', + config: { + apiProvider: 'OpenAI', + }, + }, +]; + +(useLoadConnectors as jest.Mock).mockReturnValue({ + data: mockConnectors, + error: null, + isSuccess: true, +}); + +describe('ConnectorSelectorInline', () => { + it('renders empty view if no selected conversation is provided', () => { + const { getByText } = render( + + + + ); + expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument(); + }); + + it('renders empty view if selectedConnectorId is NOT in list of connectors', () => { + const conversation: Conversation = { + id: 'conversation_id', + messages: [], + apiConfig: {}, + }; + const { getByText } = render( + + + + ); + expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument(); + }); + + it('renders selected connector if selected selectedConnectorId is in list of connectors', () => { + const conversation: Conversation = { + id: 'conversation_id', + messages: [], + apiConfig: {}, + }; + const { getByText } = render( + + + + ); + expect(getByText(mockConnectors[0].name)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx new file mode 100644 index 0000000000000..09b77c642ffcf --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx @@ -0,0 +1,286 @@ +/* + * 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiText } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; + +import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; +import { + GEN_AI_CONNECTOR_ID, + OpenAiProviderType, +} from '@kbn/stack-connectors-plugin/public/common'; +import { css } from '@emotion/css/dist/emotion-css.cjs'; +import { Conversation } from '../../..'; +import { useLoadConnectors } from '../use_load_connectors'; +import * as i18n from '../translations'; +import { useLoadActionTypes } from '../use_load_action_types'; +import { useAssistantContext } from '../../assistant_context'; +import { useConversation } from '../../assistant/use_conversation'; + +export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR'; +interface Props { + isDisabled?: boolean; + onConnectorSelectionChange: (connectorId: string, provider: OpenAiProviderType) => void; + selectedConnectorId?: string; + selectedConversation?: Conversation; + onConnectorModalVisibilityChange?: (isVisible: boolean) => void; +} + +interface Config { + apiProvider: string; +} + +const inputContainerClassName = css` + height: 32px; + + .euiSuperSelect { + width: 400px; + } + + .euiSuperSelectControl { + border: none; + box-shadow: none; + background: none; + padding-left: 0; + } + + .euiFormControlLayoutIcons { + right: 14px; + top: 2px; + } +`; + +const inputDisplayClassName = css` + overflow: hidden; + text-overflow: ellipsis; + max-width: 400px; +`; + +const placeholderButtonClassName = css` + overflow: hidden; + text-overflow: ellipsis; + max-width: 400px; + font-weight: normal; + padding-bottom: 5px; + padding-left: 0; + padding-top: 2px; +`; + +/** + * A minimal and connected version of the ConnectorSelector component used in the Settings modal. + */ +export const ConnectorSelectorInline: React.FC = React.memo( + ({ + isDisabled = false, + onConnectorModalVisibilityChange, + selectedConnectorId, + selectedConversation, + onConnectorSelectionChange, + }) => { + const [isOpen, setIsOpen] = useState(false); + const { actionTypeRegistry, http } = useAssistantContext(); + const { setApiConfig } = useConversation(); + // Connector Modal State + const [isConnectorModalVisible, setIsConnectorModalVisible] = useState(false); + const { data: actionTypes } = useLoadActionTypes({ http }); + const actionType = actionTypes?.find((at) => at.id === GEN_AI_CONNECTOR_ID) ?? { + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['general'], + isSystemActionType: false, + id: '.gen-ai', + name: 'Generative AI', + enabled: true, + }; + + const { + data: connectors, + isLoading: isLoadingActionTypes, + isFetching: isFetchingActionTypes, + refetch: refetchConnectors, + } = useLoadConnectors({ http }); + const isLoading = isLoadingActionTypes || isFetchingActionTypes; + const selectedConnectorName = + connectors?.find((c) => c.id === selectedConnectorId)?.name ?? + i18n.INLINE_CONNECTOR_PLACEHOLDER; + + const addNewConnectorOption = useMemo(() => { + return { + value: ADD_NEW_CONNECTOR, + inputDisplay: i18n.ADD_NEW_CONNECTOR, + dropdownDisplay: ( + + + + {i18n.ADD_NEW_CONNECTOR} + + + + {/* Right offset to compensate for 'selected' icon of EuiSuperSelect since native footers aren't supported*/} +

+ + + ), + }; + }, []); + + const connectorOptions = useMemo(() => { + return ( + connectors?.map((connector) => { + const apiProvider: string | undefined = ( + connector as ActionConnectorProps + )?.config?.apiProvider; + return { + value: connector.id, + inputDisplay: ( + + {connector.name} + + ), + dropdownDisplay: ( + + {connector.name} + {apiProvider && ( + +

{apiProvider}

+
+ )} +
+ ), + }; + }) ?? [] + ); + }, [connectors]); + + const cleanupAndCloseModal = useCallback(() => { + onConnectorModalVisibilityChange?.(false); + setIsConnectorModalVisible(false); + }, [onConnectorModalVisibilityChange]); + + const onConnectorClick = useCallback(() => { + setIsOpen(!isOpen); + }, [isOpen]); + + const handleOnBlur = useCallback(() => setIsOpen(false), []); + + const onChange = useCallback( + (connectorId: string, apiProvider?: OpenAiProviderType) => { + setIsOpen(false); + + if (connectorId === ADD_NEW_CONNECTOR) { + onConnectorModalVisibilityChange?.(true); + setIsConnectorModalVisible(true); + return; + } + + const provider = + apiProvider ?? + ((connectors?.find((c) => c.id === connectorId) as ActionConnectorProps) + ?.config.apiProvider as OpenAiProviderType); + + if (selectedConversation != null) { + setApiConfig({ + conversationId: selectedConversation.id, + apiConfig: { + ...selectedConversation.apiConfig, + connectorId, + provider, + }, + }); + } + + onConnectorSelectionChange(connectorId, provider); + }, + [ + connectors, + selectedConversation, + onConnectorSelectionChange, + onConnectorModalVisibilityChange, + setApiConfig, + ] + ); + + const placeholderComponent = useMemo( + () => ( + + {i18n.INLINE_CONNECTOR_PLACEHOLDER} + + ), + [] + ); + + return ( + + + + {i18n.INLINE_CONNECTOR_LABEL} + + + + {isOpen ? ( + + ) : ( + + + {selectedConnectorName} + + + )} + {isConnectorModalVisible && ( + { + const provider = (savedAction as ActionConnectorProps)?.config + .apiProvider as OpenAiProviderType; + onChange(savedAction.id, provider); + onConnectorSelectionChange(savedAction.id, provider); + refetchConnectors?.(); + cleanupAndCloseModal(); + }} + actionTypeRegistry={actionTypeRegistry} + /> + )} + + + ); + } +); + +ConnectorSelectorInline.displayName = 'ConnectorSelectorInline'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts index df6343f62b4e2..2c8523f2966b9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts @@ -45,6 +45,20 @@ export const ADD_NEW_CONNECTOR = i18n.translate( } ); +export const INLINE_CONNECTOR_LABEL = i18n.translate( + 'xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorLabel', + { + defaultMessage: 'Connector:', + } +); + +export const INLINE_CONNECTOR_PLACEHOLDER = i18n.translate( + 'xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder', + { + defaultMessage: 'Select a Connector', + } +); + export const ADD_CONNECTOR_TITLE = i18n.translate( 'xpack.elasticAssistant.assistant.connectors.addConnectorButton.title', { diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx index b7faa54202c74..ac8598c427026 100644 --- a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx +++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx @@ -4,15 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiImage, - EuiText, - EuiTitle, - useEuiTheme, -} from '@elastic/eui'; +import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiImage, EuiTitle, useEuiTheme } from '@elastic/eui'; import React from 'react'; import { css } from '@emotion/react'; import { withLink } from '../links'; @@ -96,11 +88,8 @@ export const LandingLinksImageCards: React.FC = React.m {isBeta && }
} - description={ - - {description} - - } + titleElement="span" + description={{description}} />
); diff --git a/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx index 8f73bef94e581..1c232f14e7187 100644 --- a/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx +++ b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx @@ -53,7 +53,12 @@ function ConfigurationValueColumn({ {value && ( copyToClipboard(value)} diff --git a/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx b/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx index 6d00acd9af7be..9ae2df7fb5382 100644 --- a/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx +++ b/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx @@ -7,9 +7,11 @@ import { i18n } from '@kbn/i18n'; import { + copyToClipboard, EuiBasicTable, EuiBasicTableColumn, EuiButton, + EuiButtonIcon, EuiLink, EuiMarkdownFormat, EuiSpacer, @@ -144,9 +146,24 @@ function ConfigurationValueColumn({ } return ( - - {value} - + <> + + {value} + + {value && ( + copyToClipboard(value)} + /> + )} + ); } diff --git a/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx b/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx index f7fc8eed040cc..786770091f0f7 100644 --- a/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx +++ b/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx @@ -14,6 +14,7 @@ import { EuiSpacer, } from '@elastic/eui'; import React, { useState } from 'react'; +import { useEuiTheme } from '@elastic/eui'; import { INSTRUCTION_VARIANT, getDisplayText, @@ -44,10 +45,11 @@ export function InstructionsSet({ const onSelectedTabChange = (tab: string) => { setSelectedTab(tab); }; + const { euiTheme } = useEuiTheme(); function InstructionTabs({ agentTabs }: { agentTabs: AgentTab[] }) { return ( - + {agentTabs.map((tab) => ( ( - - {value} - + <> + + {value} + + {value && ( + copyToClipboard(value)} + /> + )} + ), }, { diff --git a/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts b/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts index 30fb927bf5162..6d46fe8fd230a 100644 --- a/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts +++ b/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts @@ -8,30 +8,34 @@ /* eslint-disable no-console */ import datemath from '@elastic/datemath'; +import { errors } from '@elastic/elasticsearch'; +import { AxiosError } from 'axios'; import yargs from 'yargs'; import { initDiagnosticsBundle } from './diagnostics_bundle'; const { argv } = yargs(process.argv.slice(2)) .option('esHost', { - demandOption: true, type: 'string', description: 'Elasticsearch host name', }) .option('kbHost', { - demandOption: true, type: 'string', description: 'Kibana host name', }) .option('username', { - demandOption: true, type: 'string', description: 'Kibana host name', }) .option('password', { - demandOption: true, type: 'string', description: 'Kibana host name', }) + .option('cloudId', { + type: 'string', + }) + .option('apiKey', { + type: 'string', + }) .option('rangeFrom', { type: 'string', description: 'Time-range start', @@ -48,10 +52,20 @@ const { argv } = yargs(process.argv.slice(2)) }) .help(); -const { esHost, kbHost, password, username, kuery } = argv; +const { esHost, kbHost, password, username, kuery, apiKey, cloudId } = argv; const rangeFrom = argv.rangeFrom as unknown as number; const rangeTo = argv.rangeTo as unknown as number; +if ((!esHost || !kbHost) && !cloudId) { + console.error('Either esHost and kbHost or cloudId must be provided'); + process.exit(1); +} + +if ((!username || !password) && !apiKey) { + console.error('Either username and password or apiKey must be provided'); + process.exit(1); +} + if (rangeFrom) { console.log(`rangeFrom = ${new Date(rangeFrom).toISOString()}`); } @@ -64,6 +78,8 @@ initDiagnosticsBundle({ esHost, kbHost, password, + apiKey, + cloudId, username, start: rangeFrom, end: rangeTo, @@ -73,7 +89,20 @@ initDiagnosticsBundle({ console.log(res); }) .catch((err) => { - console.log(err); + process.exitCode = 1; + if (err instanceof AxiosError && err.response?.data) { + console.error(err.response.data); + return; + } + + // @ts-expect-error + if (err instanceof errors.ResponseError && err.meta.body.error.reason) { + // @ts-expect-error + console.error(err.meta.body.error.reason); + return; + } + + console.error(err); }); function convertDate(dateString: string): number { diff --git a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts index af1de9a98988e..92fe9f08e260b 100644 --- a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts +++ b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts @@ -19,27 +19,43 @@ type DiagnosticsBundle = APIReturnType<'GET /internal/apm/diagnostics'>; export async function initDiagnosticsBundle({ esHost, kbHost, + cloudId, username, password, + apiKey, start, end, kuery, }: { - esHost: string; - kbHost: string; - username: string; - password: string; + esHost?: string; + kbHost?: string; + cloudId?: string; start: number | undefined; end: number | undefined; kuery: string | undefined; + username?: string; + password?: string; + apiKey?: string; }) { - const esClient = new Client({ node: esHost, auth: { username, password } }); + const auth = username && password ? { username, password } : undefined; + const apiKeyHeader = apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}; + const { kibanaHost } = parseCloudId(cloudId); + + const esClient = new Client({ + ...(esHost ? { node: esHost } : {}), + ...(cloudId ? { cloud: { id: cloudId } } : {}), + auth, + headers: { ...apiKeyHeader }, + }); const kibanaClient = axios.create({ - baseURL: kbHost, - auth: { username, password }, + baseURL: kbHost ?? kibanaHost, + auth, + // @ts-expect-error + headers: { 'kbn-xsrf': 'true', ...apiKeyHeader }, }); const apmIndices = await getApmIndices(kibanaClient); + const bundle = await getDiagnosticsBundle({ esClient, apmIndices, @@ -99,3 +115,19 @@ async function getKibanaVersion(kibanaClient: AxiosInstance) { const res = await kibanaClient.get('/api/status'); return res.data.version.number; } + +function parseCloudId(cloudId?: string) { + if (!cloudId) { + return {}; + } + + const [instanceAlias, encodedString] = cloudId.split(':'); + const decodedString = Buffer.from(encodedString, 'base64').toString('utf8'); + const [hostname, esId, kbId] = decodedString.split('$'); + + return { + kibanaHost: `https://${kbId}.${hostname}`, + esHost: `https://${esId}.${hostname}`, + instanceAlias, + }; +} diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx index e41067dddc1ca..33c893d6f6b46 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx @@ -134,6 +134,26 @@ describe('EditCategory ', () => { await waitFor(() => expect(onSubmit).toBeCalledWith('new')); }); + it('should trim category', async () => { + appMockRender.render(); + + userEvent.click(screen.getByTestId('category-edit-button')); + + await waitFor(() => { + expect(screen.getByTestId('categories-list')).toBeInTheDocument(); + }); + + userEvent.type(screen.getByRole('combobox'), 'category-with-space {enter}'); + + await waitFor(() => { + expect(screen.getByTestId('edit-category-submit')).not.toBeDisabled(); + }); + + userEvent.click(screen.getByTestId('edit-category-submit')); + + await waitFor(() => expect(onSubmit).toBeCalledWith('category-with-space')); + }); + it('should not save category on cancel click', async () => { appMockRender.render(); diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx index df3c3e538e71a..f121c16f3efe8 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx @@ -95,9 +95,7 @@ export const EditCategory = React.memo(({ isLoading, onSubmit, category }: EditC const { isValid, data } = await formState.submit(); if (isValid) { - const newCategory = data.category != null ? data.category : null; - - onSubmit(newCategory); + onSubmit(data.category?.trim() ?? null); } setIsEditCategory(false); diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx index 28198f7f9c28c..8b87e80884ad3 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx @@ -82,6 +82,22 @@ describe('EditTags ', () => { await waitFor(() => expect(onSubmit).toBeCalledWith(['dude'])); }); + it('trims the tags on submit', async () => { + appMockRender.render(); + + userEvent.click(screen.getByTestId('tag-list-edit-button')); + + await waitFor(() => { + expect(screen.getByTestId('edit-tags')).toBeInTheDocument(); + }); + + userEvent.type(screen.getByRole('combobox'), 'dude {enter}'); + + userEvent.click(screen.getByTestId('edit-tags-submit')); + + await waitFor(() => expect(onSubmit).toBeCalledWith(['dude'])); + }); + it('cancels on cancel', async () => { appMockRender.render(); diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx index 66db38bb500c5..07bf8ac11c39f 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx @@ -79,7 +79,9 @@ export const EditTags = React.memo(({ isLoading, onSubmit, tags }: EditTagsProps const onSubmitTags = useCallback(async () => { const { isValid, data: newData } = await submit(); if (isValid && newData.tags) { - onSubmit(newData.tags); + const trimmedTags = newData.tags.map((tag: string) => tag.trim()); + + onSubmit(trimmedTags); form.reset({ defaultValue: newData }); setIsEditTags(false); } diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 758d9d950ce9f..3703ba05fe486 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -306,34 +306,6 @@ describe('Create case', () => { }); }); - it('does not submits the title when the length is longer than 160 characters', async () => { - const longTitle = 'a'.repeat(161); - - appMockRender.render( - - - - - ); - - await waitForFormToRender(screen); - - const titleInput = within(screen.getByTestId('caseTitle')).getByTestId('input'); - userEvent.paste(titleInput, longTitle); - - userEvent.click(screen.getByTestId('create-case-submit')); - - await waitFor(() => { - expect( - screen.getByText( - 'The length of the name is too long. The maximum length is 160 characters.' - ) - ).toBeInTheDocument(); - }); - - expect(postCase).not.toHaveBeenCalled(); - }); - it('should toggle sync settings', async () => { useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index f8c1a50392889..4df39cb2d52ac 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -71,6 +71,24 @@ export const FormContext: React.FC = ({ const { startTransaction } = useCreateCaseWithAttachmentsTransaction(); const availableOwners = useAvailableCasesOwners(); + const trimUserFormData = (userFormData: CaseUI) => { + let formData = { + ...userFormData, + title: userFormData.title.trim(), + description: userFormData.description.trim(), + }; + + if (userFormData.category) { + formData = { ...formData, category: userFormData.category.trim() }; + } + + if (userFormData.tags) { + formData = { ...formData, tags: userFormData.tags.map((tag: string) => tag.trim()) }; + } + + return formData; + }; + const submitCase = useCallback( async ( { @@ -92,9 +110,11 @@ export const FormContext: React.FC = ({ ? normalizeActionConnector(caseConnector, fields) : getNoneConnector(); + const trimmedData = trimUserFormData(userFormData); + const theCase = await postCase({ request: { - ...userFormData, + ...trimmedData, connector: connectorToUpdate, settings: { syncAlerts }, owner: selectedOwner ?? defaultOwner, diff --git a/x-pack/plugins/cases/public/components/description/index.test.tsx b/x-pack/plugins/cases/public/components/description/index.test.tsx index 5515753736274..33145c711493d 100644 --- a/x-pack/plugins/cases/public/components/description/index.test.tsx +++ b/x-pack/plugins/cases/public/components/description/index.test.tsx @@ -110,38 +110,6 @@ describe('Description', () => { }); }); - it('shows an error when description is empty', async () => { - const res = appMockRender.render( - - ); - - userEvent.click(res.getByTestId('description-edit-icon')); - - userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea')); - userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), ''); - - await waitFor(() => { - expect(screen.getByText('A description is required.')).toBeInTheDocument(); - expect(screen.getByTestId('editable-save-markdown')).toHaveAttribute('disabled'); - }); - }); - - it('shows an error when description is a sting of empty characters', async () => { - const res = appMockRender.render( - - ); - - userEvent.click(res.getByTestId('description-edit-icon')); - - userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea')); - userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), ' '); - - await waitFor(() => { - expect(screen.getByText('A description is required.')).toBeInTheDocument(); - expect(screen.getByTestId('editable-save-markdown')).toHaveAttribute('disabled'); - }); - }); - it('shows an error when description is too long', async () => { const longDescription = Array(MAX_DESCRIPTION_LENGTH / 2 + 1) .fill('a') @@ -184,12 +152,6 @@ describe('Description', () => { sessionStorage.setItem(draftStorageKey, 'value set in storage'); }); - it('should show unsaved draft message correctly', async () => { - appMockRender.render(); - - expect(screen.getByTestId('description-unsaved-draft')).toBeInTheDocument(); - }); - it('should not show unsaved draft message when loading', async () => { appMockRender.render( diff --git a/x-pack/plugins/cases/public/components/description/index.tsx b/x-pack/plugins/cases/public/components/description/index.tsx index 3574f21f87ce3..a3e074f6f5867 100644 --- a/x-pack/plugins/cases/public/components/description/index.tsx +++ b/x-pack/plugins/cases/public/components/description/index.tsx @@ -113,7 +113,7 @@ export const Description = ({ const handleOnSave = useCallback( (content: string) => { - onUpdateField({ key: DESCRIPTION_ID, value: content }); + onUpdateField({ key: DESCRIPTION_ID, value: content.trim() }); setIsEditable(false); }, [onUpdateField, setIsEditable] diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx index f7e4df4d6a2e2..9f75a94274aaf 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx @@ -185,6 +185,33 @@ describe('EditableTitle', () => { ).toBe(true); }); + it('trims the title before submit', () => { + const newTitle = 'new test title with spaces '; + + const wrapper = mount( + + + + ); + + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); + wrapper.update(); + + wrapper + .find('input[data-test-subj="editable-title-input-field"]') + .last() + .simulate('change', { target: { value: newTitle } }); + + wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click'); + wrapper.update(); + + expect(submitTitle).toHaveBeenCalled(); + expect(submitTitle.mock.calls[0][0]).toEqual(newTitle.trim()); + expect( + wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists() + ).toBe(true); + }); + it('does not submit the title when the length is longer than 160 characters', () => { const longTitle = 'a'.repeat(161); diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index 61672e70d8e3d..b53d12980c78a 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -16,7 +16,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { mockCases } from '../../mocks'; import { createCasesClientMockArgs } from '../mocks'; import { create } from './create'; -import { CaseSeverity, ConnectorTypes } from '../../../common/types/domain'; +import { CaseSeverity, CaseStatuses, ConnectorTypes } from '../../../common/types/domain'; describe('create', () => { const theCase = { @@ -151,6 +151,31 @@ describe('create', () => { 'Failed to create case: Error: The title field cannot be an empty string.' ); }); + + it('should trim title', async () => { + await create({ ...theCase, title: 'title with spaces ' }, clientArgs); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + title: 'title with spaces', + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + category: null, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); describe('description', () => { @@ -188,6 +213,34 @@ describe('create', () => { 'Failed to create case: Error: The description field cannot be an empty string.' ); }); + + it('should trim description', async () => { + await create( + { ...theCase, description: 'this is a description with spaces!! ' }, + clientArgs + ); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + description: 'this is a description with spaces!!', + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + category: null, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); describe('tags', () => { @@ -235,6 +288,31 @@ describe('create', () => { `Failed to create case: Error: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` ); }); + + it('should trim tags', async () => { + await create({ ...theCase, tags: ['pepsi ', 'coke'] }, clientArgs); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + tags: ['pepsi', 'coke'], + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + category: null, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); describe('Category', () => { @@ -269,5 +347,29 @@ describe('create', () => { 'Failed to create case: Error: The category field cannot be an empty string.,Invalid value " " supplied to "category"' ); }); + + it('should trim category', async () => { + await create({ ...theCase, category: 'reporting ' }, clientArgs); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + category: 'reporting', + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); }); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 30524c1db6595..f1ea52dcb45c9 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -61,10 +61,22 @@ export const create = async (data: CasePostRequest, clientArgs: CasesClientArgs) licensingService.notifyUsage(LICENSING_CASE_ASSIGNMENT_FEATURE); } + /** + * Trim title, category, description and tags before saving to ES + */ + + const trimmedQuery = { + ...query, + title: query.title.trim(), + description: query.description.trim(), + category: query.category?.trim() ?? null, + tags: query.tags?.map((tag) => tag.trim()) ?? [], + }; + const newCase = await caseService.postNewCase({ attributes: transformNewCase({ user, - newCase: query, + newCase: trimmedQuery, }), id: savedObjectID, refresh: false, diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts index 17b51658b1233..f78e198ddeb52 100644 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/update.test.ts @@ -394,6 +394,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The category field cannot be an empty string.,Invalid value " " supplied to "cases,category"' ); }); + + it('should trim category', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + category: 'security ', + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + category: 'security', + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Title', () => { @@ -488,6 +523,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The title field cannot be an empty string.' ); }); + + it('should trim title', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + title: 'title with spaces ', + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + title: 'title with spaces', + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Description', () => { @@ -585,6 +655,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The description field cannot be an empty string.' ); }); + + it('should trim description', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + description: 'This is a description with spaces!! ', + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + description: 'This is a description with spaces!!', + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Tags', () => { @@ -724,6 +829,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The tag field cannot be an empty string.' ); }); + + it('should trim tags', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + tags: ['coke ', 'pepsi'], + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + tags: ['coke', 'pepsi'], + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Validation', () => { diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 121fd2a8e3aa5..64c0f170517fa 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -462,6 +462,36 @@ export const update = async ( } }; +const trimCaseAttributes = ( + updateCaseAttributes: Omit +) => { + let trimmedAttributes = { ...updateCaseAttributes }; + + if (updateCaseAttributes.title) { + trimmedAttributes = { ...trimmedAttributes, title: updateCaseAttributes.title.trim() }; + } + + if (updateCaseAttributes.description) { + trimmedAttributes = { + ...trimmedAttributes, + description: updateCaseAttributes.description.trim(), + }; + } + + if (updateCaseAttributes.category) { + trimmedAttributes = { ...trimmedAttributes, category: updateCaseAttributes.category.trim() }; + } + + if (updateCaseAttributes.tags) { + trimmedAttributes = { + ...trimmedAttributes, + tags: updateCaseAttributes.tags.map((tag: string) => tag.trim()), + }; + } + + return trimmedAttributes; +}; + const createPatchCasesPayload = ({ casesToUpdate, user, @@ -478,19 +508,21 @@ const createPatchCasesPayload = ({ const dedupedAssignees = dedupAssignees(assignees); + const trimmedCaseAttributes = trimCaseAttributes(updateCaseAttributes); + return { caseId, originalCase, updatedAttributes: { - ...updateCaseAttributes, + ...trimmedCaseAttributes, ...(dedupedAssignees && { assignees: dedupedAssignees }), ...getClosedInfoForUpdate({ user, closedDate: updatedDt, - status: updateCaseAttributes.status, + status: trimmedCaseAttributes.status, }), ...getDurationForUpdate({ - status: updateCaseAttributes.status, + status: trimmedCaseAttributes.status, closedAt: updatedDt, createdAt: originalCase.attributes.created_at, }), diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 7b9db8cfe5dbe..5ba3648638e61 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -21,6 +21,11 @@ export const BENCHMARKS_API_CURRENT_VERSION = '1'; export const FIND_CSP_RULE_TEMPLATE_ROUTE_PATH = '/internal/cloud_security_posture/rules/_find'; export const FIND_CSP_RULE_TEMPLATE_API_CURRENT_VERSION = '1'; +export const DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION = '1'; + +export const GET_DETECTION_RULE_ALERTS_STATUS_PATH = + '/internal/cloud_security_posture/detection_engine_rules/alerts/_status'; + export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture'; // TODO: REMOVE CSP_LATEST_FINDINGS_DATA_VIEW and replace it with LATEST_FINDINGS_INDEX_PATTERN export const CSP_LATEST_FINDINGS_DATA_VIEW = 'logs-cloud_security_posture.findings_latest-*'; @@ -136,3 +141,5 @@ export const AWS_CREDENTIALS_TYPE_TO_FIELDS_MAP: AwsCredentialsTypeFieldMap = { export const SETUP_ACCESS_CLOUD_SHELL = 'google_cloud_shell'; export const SETUP_ACCESS_MANUAL = 'manual'; + +export const DETECTION_ENGINE_ALERTS_INDEX_DEFAULT = '.alerts-security.alerts-default'; diff --git a/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts b/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts index dace996d86906..bcbe41cdc96bc 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts @@ -15,7 +15,6 @@ export const getSafeVulnerabilitiesQueryFilter = (query?: QueryDslQueryContainer { exists: { field: 'vulnerability.score.base' } }, { exists: { field: 'vulnerability.score.version' } }, { exists: { field: 'resource.id' } }, - { exists: { field: 'resource.name' } }, ], }, }); diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts index 8a584be1bdbac..a73e75d706e72 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts @@ -5,46 +5,11 @@ * 2.0. */ import { HttpSetup } from '@kbn/core/public'; +import { RuleCreateProps, RuleResponse } from '../types'; const DETECTION_ENGINE_URL = '/api/detection_engine' as const; const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules` as const; -interface RuleCreateProps { - type: string; - language: string; - license: string; - author: string[]; - filters: any[]; - false_positives: any[]; - risk_score: number; - risk_score_mapping: any[]; - severity: string; - severity_mapping: any[]; - threat: any[]; - interval: string; - from: string; - to: string; - timestamp_override: string; - timestamp_override_fallback_disabled: boolean; - actions: any[]; - enabled: boolean; - alert_suppression: { - group_by: string[]; - missing_fields_strategy: string; - }; - index: string[]; - query: string; - references: string[]; - name: string; - description: string; - tags: string[]; - max_signals: number; -} - -export interface RuleResponse extends RuleCreateProps { - id: string; -} - export const createDetectionRule = async ({ http, rule, diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/index.ts b/x-pack/plugins/cloud_security_posture/public/common/api/index.ts index fb3caf4fa9814..29320516d2842 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/index.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/index.ts @@ -6,3 +6,4 @@ */ export * from './use_stats_api'; +export * from './use_fetch_detection_rules_by_tags'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_alerts_status.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_alerts_status.ts new file mode 100644 index 0000000000000..efea6629b7743 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_alerts_status.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useQuery } from '@tanstack/react-query'; +import { + DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + GET_DETECTION_RULE_ALERTS_STATUS_PATH, +} from '../../../common/constants'; +import { DETECTION_ENGINE_ALERTS_KEY } from '../constants'; + +interface AlertStatus { + acknowledged: number; + closed: number; + open: number; + total: number; +} + +export const useFetchDetectionRulesAlertsStatus = (tags: string[]) => { + const { http } = useKibana().services; + + if (!http) { + throw new Error('Kibana http service is not available'); + } + + return useQuery([DETECTION_ENGINE_ALERTS_KEY, tags], () => + http.get(GET_DETECTION_RULE_ALERTS_STATUS_PATH, { + version: DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + query: { tags }, + }) + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_by_tags.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_by_tags.ts new file mode 100644 index 0000000000000..953b31b1b5428 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_by_tags.ts @@ -0,0 +1,53 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useQuery } from '@tanstack/react-query'; +import { RuleResponse } from '../types'; +import { DETECTION_ENGINE_RULES_KEY } from '../constants'; + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: RuleResponse[]; +} + +export const TAGS_FIELD = 'alert.attributes.tags'; + +const DETECTION_ENGINE_URL = '/api/detection_engine' as const; +const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules` as const; +export const DETECTION_ENGINE_RULES_URL_FIND = `${DETECTION_ENGINE_RULES_URL}/_find` as const; + +export function convertRuleTagsToKQL(tags: string[]): string { + return `${TAGS_FIELD}:(${tags.map((tag) => `"${tag}"`).join(' AND ')})`; +} + +export const useFetchDetectionRulesByTags = (tags: string[]) => { + const { http } = useKibana().services; + + const query = { + page: 1, + per_page: 1, + filter: convertRuleTagsToKQL(tags), + }; + + return useQuery([DETECTION_ENGINE_RULES_KEY, tags], () => + http.fetch(DETECTION_ENGINE_RULES_URL_FIND, { + method: 'GET', + query, + }) + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 57cefa02344ea..8f51456c009e4 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -217,3 +217,6 @@ export const FINDINGS_DOCS_URL = 'https://ela.st/findings'; export const MIN_VERSION_GCP_CIS = '1.5.0'; export const NO_FINDINGS_STATUS_REFRESH_INTERVAL_MS = 10000; + +export const DETECTION_ENGINE_RULES_KEY = 'detection_engine_rules'; +export const DETECTION_ENGINE_ALERTS_KEY = 'detection_engine_alerts'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/types.ts b/x-pack/plugins/cloud_security_posture/public/common/types.ts index 0933d8cbda260..d9bdc58cd3bb3 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/types.ts @@ -36,3 +36,46 @@ export interface CspFindingsQueryData { } export type Sort = NonNullable['sort']>; + +interface RuleSeverityMapping { + field: string; + value: string; + operator: 'equals'; + severity: string; +} + +export interface RuleCreateProps { + type: string; + language: string; + license: string; + author: string[]; + filters: unknown[]; + false_positives: unknown[]; + risk_score: number; + risk_score_mapping: unknown[]; + severity: string; + severity_mapping: RuleSeverityMapping[]; + threat: unknown[]; + interval: string; + from: string; + to: string; + timestamp_override: string; + timestamp_override_fallback_disabled: boolean; + actions: unknown[]; + enabled: boolean; + alert_suppression: { + group_by: string[]; + missing_fields_strategy: string; + }; + index: string[]; + query: string; + references: string[]; + name: string; + description: string; + tags: string[]; + max_signals: number; +} + +export interface RuleResponse extends RuleCreateProps { + id: string; +} diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts index c4d1e00450873..b8c6adf2063a7 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts @@ -5,17 +5,13 @@ * 2.0. */ -import type { CspVulnerabilityFinding } from '../../../common/schemas'; +import type { Vulnerability } from '../../../common/schemas'; -export const getVulnerabilityReferenceUrl = ( - finding: CspVulnerabilityFinding -): string | undefined => { +export const getVulnerabilityReferenceUrl = (vulnerability: Vulnerability): string | undefined => { const nvdDomain = 'https://nvd'; - const nvdWebsite = `${nvdDomain}.nist.gov/vuln/detail/${finding?.vulnerability?.id}`; + const nvdWebsite = `${nvdDomain}.nist.gov/vuln/detail/${vulnerability?.id}`; - const vulnerabilityReference = finding.vulnerability?.cvss?.nvd - ? nvdWebsite - : finding.vulnerability?.reference; + const vulnerabilityReference = vulnerability?.cvss?.nvd ? nvdWebsite : vulnerability?.reference; return vulnerabilityReference; }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx new file mode 100644 index 0000000000000..0ee3cd24d36e1 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink, EuiLoadingSpinner, EuiSkeletonText, EuiText } from '@elastic/eui'; +import type { HttpSetup } from '@kbn/core/public'; +import { useHistory } from 'react-router-dom'; +import useSessionStorage from 'react-use/lib/useSessionStorage'; +import { useQueryClient } from '@tanstack/react-query'; +import { useFetchDetectionRulesAlertsStatus } from '../common/api/use_fetch_detection_rules_alerts_status'; +import { useFetchDetectionRulesByTags } from '../common/api/use_fetch_detection_rules_by_tags'; +import { RuleResponse } from '../common/types'; +import { useKibana } from '../common/hooks/use_kibana'; +import { showSuccessToast } from './take_action'; +import { DETECTION_ENGINE_ALERTS_KEY, DETECTION_ENGINE_RULES_KEY } from '../common/constants'; + +const RULES_PAGE_PATH = '/rules/management'; +const ALERTS_PAGE_PATH = '/alerts'; + +const RULES_TABLE_SESSION_STORAGE_KEY = 'securitySolution.rulesTable'; + +interface DetectionRuleCounterProps { + tags: string[]; + createRuleFn: (http: HttpSetup) => Promise; +} + +export const DetectionRuleCounter = ({ tags, createRuleFn }: DetectionRuleCounterProps) => { + const { data: rulesData, isLoading: ruleIsLoading } = useFetchDetectionRulesByTags(tags); + const { data: alertsData, isLoading: alertsIsLoading } = useFetchDetectionRulesAlertsStatus(tags); + + const [isCreateRuleLoading, setIsCreateRuleLoading] = useState(false); + + const queryClient = useQueryClient(); + const { http, notifications } = useKibana().services; + + const history = useHistory(); + + const [, setRulesTable] = useSessionStorage(RULES_TABLE_SESSION_STORAGE_KEY); + + const rulePageNavigation = useCallback(async () => { + await setRulesTable({ + tags, + }); + history.push({ + pathname: RULES_PAGE_PATH, + }); + }, [history, setRulesTable, tags]); + + const alertsPageNavigation = useCallback(() => { + history.push({ + pathname: ALERTS_PAGE_PATH, + }); + }, [history]); + + const createDetectionRuleOnClick = useCallback(async () => { + setIsCreateRuleLoading(true); + const ruleResponse = await createRuleFn(http); + setIsCreateRuleLoading(false); + showSuccessToast(notifications, http, ruleResponse); + // Triggering a refetch of rules and alerts to update the UI + queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]); + queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]); + }, [createRuleFn, http, notifications, queryClient]); + + return ( + + {rulesData?.total === 0 ? ( + <> + + {isCreateRuleLoading ? ( + <> + {' '} + + + ) : ( + <> + + + {' '} + + + )} + + + ) : ( + <> + + + {' '} + {' '} + + + + + )} + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx index 1472a48faccfc..7fafe17113c84 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx @@ -18,7 +18,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; -import { PackageInfo } from '@kbn/fleet-plugin/common'; +import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; @@ -34,6 +34,7 @@ import { NewPackagePolicyPostureInput, } from '../utils'; import { SetupFormat, useAwsCredentialsForm } from './hooks'; +import { AWS_ORGANIZATION_ACCOUNT } from '../policy_template_form'; import { AwsCredentialsType } from '../../../../common/types'; interface AWSSetupInfoContentProps { @@ -106,8 +107,10 @@ interface Props { const CloudFormationSetup = ({ hasCloudFormationTemplate, + input, }: { hasCloudFormationTemplate: boolean; + input: NewPackagePolicyInput; }) => { if (!hasCloudFormationTemplate) { return ( @@ -119,6 +122,9 @@ const CloudFormationSetup = ({ ); } + + const accountType = input.streams?.[0]?.vars?.['aws.account_type']?.value; + return ( <> @@ -127,12 +133,21 @@ const CloudFormationSetup = ({ list-style: auto; `} > -
  • - -
  • + {accountType === AWS_ORGANIZATION_ACCOUNT ? ( +
  • + +
  • + ) : ( +
  • + +
  • + )}
  • {setupFormat === 'cloud_formation' && ( - + )} {setupFormat === 'manual' && ( <> diff --git a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx index 57684d02fd157..f4035f1532b7c 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx @@ -17,22 +17,66 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import type { HttpSetup } from '@kbn/core/public'; +import type { HttpSetup, NotificationsStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useQueryClient } from '@tanstack/react-query'; +import type { RuleResponse } from '../common/types'; import { CREATE_RULE_ACTION_SUBJ, TAKE_ACTION_SUBJ } from './test_subjects'; import { useKibana } from '../common/hooks/use_kibana'; -import type { RuleResponse } from '../common/api/create_detection_rule'; +import { DETECTION_ENGINE_ALERTS_KEY, DETECTION_ENGINE_RULES_KEY } from '../common/constants'; const RULE_PAGE_PATH = '/app/security/rules/id/'; interface TakeActionProps { createRuleFn: (http: HttpSetup) => Promise; } + +export const showSuccessToast = ( + notifications: NotificationsStart, + http: HttpSetup, + ruleResponse: RuleResponse +) => { + return notifications.toasts.addSuccess({ + toastLifeTimeMs: 10000, + color: 'success', + iconType: '', + text: toMountPoint( +
    + + {ruleResponse.name} + {` `} + + + + + + + + + + + + +
    + ), + }); +}; + /* * This component is used to create a detection rule from Flyout. * It accepts a createRuleFn parameter which is used to create a rule in a generic way. */ export const TakeAction = ({ createRuleFn }: TakeActionProps) => { + const queryClient = useQueryClient(); const [isPopoverOpen, setPopoverOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const closePopover = () => { @@ -45,42 +89,6 @@ export const TakeAction = ({ createRuleFn }: TakeActionProps) => { const { http, notifications } = useKibana().services; - const showSuccessToast = (ruleResponse: RuleResponse) => { - return notifications.toasts.addSuccess({ - toastLifeTimeMs: 10000, - color: 'success', - iconType: '', - text: toMountPoint( -
    - - {ruleResponse.name} - {` `} - - - - - - - - - - - - -
    - ), - }); - }; - const button = ( { setIsLoading(true); const ruleResponse = await createRuleFn(http); setIsLoading(false); - showSuccessToast(ruleResponse); + showSuccessToast(notifications, http, ruleResponse); + // Triggering a refetch of rules and alerts to update the UI + queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]); + queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]); }} data-test-subj={CREATE_RULE_ACTION_SUBJ} > diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_detection_rule_counter.tsx new file mode 100644 index 0000000000000..5586f2a20126c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_detection_rule_counter.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import React from 'react'; +import { CspFinding } from '../../../../common/schemas/csp_finding'; +import { DetectionRuleCounter } from '../../../components/detection_rule_counter'; +import { createDetectionRuleFromFinding } from '../utils/create_detection_rule_from_finding'; + +export const FindingsDetectionRuleCounter = ({ finding }: { finding: CspFinding }) => { + const createMisconfigurationRuleFn = async (http: HttpSetup) => + await createDetectionRuleFromFinding(http, finding); + + return ( + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx index 81f160d03d820..cb906b99ef21b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx @@ -29,6 +29,7 @@ import { useLatestFindingsDataView } from '../../../common/api/use_latest_findin import { useKibana } from '../../../common/hooks/use_kibana'; import { CspFinding } from '../../../../common/schemas/csp_finding'; import { CisKubernetesIcons, CspFlyoutMarkdown, CodeBlock } from './findings_flyout'; +import { FindingsDetectionRuleCounter } from './findings_detection_rule_counter'; type Accordion = Pick & Pick; @@ -40,6 +41,12 @@ const getDetailsList = (data: CspFinding, discoverIndexLink: string | undefined) }), description: data.rule.name, }, + { + title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle', { + defaultMessage: 'Alerts', + }), + description: , + }, { title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.ruleTagsTitle', { defaultMessage: 'Rule Tags', diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/generate_findings_tags.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/generate_findings_tags.ts new file mode 100644 index 0000000000000..66da177e1cea8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/generate_findings_tags.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CspFinding } from '../../../../common/schemas/csp_finding'; + +const CSP_RULE_TAG = 'Cloud Security'; +const CNVM_RULE_TAG_USE_CASE = 'Use Case: Configuration Audit'; +const CNVM_RULE_TAG_DATA_SOURCE_PREFIX = 'Data Source: '; + +const STATIC_RULE_TAGS = [CSP_RULE_TAG, CNVM_RULE_TAG_USE_CASE]; + +export const generateFindingsTags = (finding: CspFinding) => { + return [STATIC_RULE_TAGS] + .concat(finding.rule.tags) + .concat( + finding.rule.benchmark.posture_type + ? [ + `${CNVM_RULE_TAG_DATA_SOURCE_PREFIX}${finding.rule.benchmark.posture_type.toUpperCase()}`, + ] + : [] + ) + .concat( + finding.rule.benchmark.posture_type === 'cspm' ? ['Domain: Cloud'] : ['Domain: Container'] + ) + .flat(); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts index c8cd677041b16..35a6147f539b2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts @@ -8,7 +8,7 @@ import { HttpSetup } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { getVulnerabilityReferenceUrl } from '../../../common/utils/get_vulnerability_reference_url'; -import type { CspVulnerabilityFinding } from '../../../../common/schemas'; +import type { Vulnerability } from '../../../../common/schemas'; import { LATEST_VULNERABILITIES_RETENTION_POLICY, VULNERABILITIES_INDEX_PATTERN, @@ -54,33 +54,33 @@ const STATIC_RULE_TAGS = [ CNVM_RULE_TAG_OS, ]; -const generateVulnerabilitiesTags = (finding: CspVulnerabilityFinding) => { - return [...STATIC_RULE_TAGS, finding.vulnerability.id]; +const generateVulnerabilitiesTags = (vulnerability: Vulnerability) => { + return [...STATIC_RULE_TAGS, vulnerability.id]; }; -const getVulnerabilityRuleName = (finding: CspVulnerabilityFinding) => { +const getVulnerabilityRuleName = (vulnerability: Vulnerability) => { return i18n.translate('xpack.csp.vulnerabilities.detectionRuleNamePrefix', { defaultMessage: 'Vulnerability: {vulnerabilityId}', values: { - vulnerabilityId: finding.vulnerability.id, + vulnerabilityId: vulnerability.id, }, }); }; -const generateVulnerabilitiesRuleQuery = (finding: CspVulnerabilityFinding) => { +const generateVulnerabilitiesRuleQuery = (vulnerability: Vulnerability) => { const currentTimestamp = new Date().toISOString(); - return `vulnerability.id: "${finding.vulnerability.id}" AND event.ingested >= "${currentTimestamp}"`; + return `vulnerability.id: "${vulnerability.id}" AND event.ingested >= "${currentTimestamp}"`; }; /* - * Creates a detection rule from a CspVulnerabilityFinding + * Creates a detection rule from a Vulnerability */ export const createDetectionRuleFromVulnerabilityFinding = async ( http: HttpSetup, - finding: CspVulnerabilityFinding + vulnerability: Vulnerability ) => { - const referenceUrl = getVulnerabilityReferenceUrl(finding); + const referenceUrl = getVulnerabilityReferenceUrl(vulnerability); return await createDetectionRule({ http, @@ -140,11 +140,11 @@ export const createDetectionRuleFromVulnerabilityFinding = async ( missing_fields_strategy: AlertSuppressionMissingFieldsStrategy.Suppress, }, index: [VULNERABILITIES_INDEX_PATTERN], - query: generateVulnerabilitiesRuleQuery(finding), + query: generateVulnerabilitiesRuleQuery(vulnerability), references: referenceUrl ? [referenceUrl] : [], - name: getVulnerabilityRuleName(finding), - description: finding.vulnerability.description, - tags: generateVulnerabilitiesTags(finding), + name: getVulnerabilityRuleName(vulnerability), + description: vulnerability.description, + tags: generateVulnerabilitiesTags(vulnerability), }, }); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx index 17e1469fcd60e..b92adf84d70ab 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx @@ -40,7 +40,10 @@ export const getVulnerabilitiesGridCellActions = < if (columnId === columns.cvss) { return vulnerabilityRow.vulnerability?.score.base; } - if (columnId === columns.resource) { + if (columnId === columns.resourceId) { + return vulnerabilityRow.resource?.id; + } + if (columnId === columns.resourceName) { return vulnerabilityRow.resource?.name; } if (columnId === columns.severity) { @@ -52,15 +55,9 @@ export const getVulnerabilitiesGridCellActions = < if (columnId === columns.version) { return vulnerabilityRow.vulnerability?.package?.version; } - if (columnId === columns.fix_version) { + if (columnId === columns.fixedVersion) { return vulnerabilityRow.vulnerability?.package?.fixed_version; } - if (columnId === columns.resource_id) { - return vulnerabilityRow.resource?.id; - } - if (columnId === columns.resource_name) { - return vulnerabilityRow.resource?.name; - } if (columnId === columns.region) { return vulnerabilityRow.cloud?.region; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx index 070fd9ea16242..efb2b97cc6891 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx @@ -254,9 +254,12 @@ const VulnerabilitiesDataGrid = ({ /> ); } - if (columnId === vulnerabilitiesColumns.resource) { + if (columnId === vulnerabilitiesColumns.resourceName) { return <>{vulnerabilityRow.resource?.name}; } + if (columnId === vulnerabilitiesColumns.resourceId) { + return <>{vulnerabilityRow.resource?.id}; + } if (columnId === vulnerabilitiesColumns.severity) { if (!vulnerabilityRow.vulnerability.severity) { return null; @@ -270,7 +273,7 @@ const VulnerabilitiesDataGrid = ({ if (columnId === vulnerabilitiesColumns.version) { return <>{vulnerabilityRow.vulnerability?.package?.version}; } - if (columnId === vulnerabilitiesColumns.fix_version) { + if (columnId === vulnerabilitiesColumns.fixedVersion) { return <>{vulnerabilityRow.vulnerability?.package?.fixed_version}; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx index e18e3b855b1cb..ee67c8e073b3e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx @@ -146,6 +146,7 @@ const ResourceVulnerabilitiesDataGrid = ({ if (!data?.page) { return []; } + return getVulnerabilitiesGridCellActions({ columnGridFn: getVulnerabilitiesColumnsGrid, columns: vulnerabilitiesColumns, @@ -154,7 +155,11 @@ const ResourceVulnerabilitiesDataGrid = ({ data: data.page, setUrlQuery, filters: urlQuery.filters, - }).filter((column) => column.id !== vulnerabilitiesColumns.resource); + }).filter( + (column) => + column.id !== vulnerabilitiesColumns.resourceName && + column.id !== vulnerabilitiesColumns.resourceId + ); }, [data?.page, dataView, pageSize, setUrlQuery, urlQuery.filters]); const flyoutVulnerabilityIndex = urlQuery?.vulnerabilityIndex; @@ -219,9 +224,6 @@ const ResourceVulnerabilitiesDataGrid = ({ /> ); } - if (columnId === vulnerabilitiesColumns.resource) { - return <>{vulnerabilityRow.resource?.name}; - } if (columnId === vulnerabilitiesColumns.severity) { if (!vulnerabilityRow.vulnerability.severity) { return null; @@ -235,7 +237,7 @@ const ResourceVulnerabilitiesDataGrid = ({ if (columnId === vulnerabilitiesColumns.version) { return <>{vulnerabilityRow.vulnerability?.package?.version}; } - if (columnId === vulnerabilitiesColumns.fix_version) { + if (columnId === vulnerabilitiesColumns.fixedVersion) { return <>{vulnerabilityRow.vulnerability?.package?.fixed_version}; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx index 8fd5eab3aab89..caa5cca3a3ac1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx @@ -109,7 +109,7 @@ const VulnerabilitiesByResourceDataGrid = ({ if (isFetching) return null; if (!resourceVulnerabilityRow?.resource?.id) return null; - if (columnId === vulnerabilitiesByResourceColumns.resource_id) { + if (columnId === vulnerabilitiesByResourceColumns.resourceId) { return ( ); } - if (columnId === vulnerabilitiesByResourceColumns.resource_name) { + if (columnId === vulnerabilitiesByResourceColumns.resourceName) { return <>{resourceVulnerabilityRow?.resource?.name}; } if (columnId === vulnerabilitiesByResourceColumns.region) { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts index 959d6db682959..42196f151bd07 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts @@ -9,8 +9,8 @@ import { EuiDataGridColumn, EuiDataGridColumnCellAction } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export const vulnerabilitiesByResourceColumns = { - resource_id: 'resource.id', - resource_name: 'resource.name', + resourceId: 'resource.id', + resourceName: 'resource.name', region: 'cloud.region', vulnerabilities_count: 'vulnerabilities_count', severity_map: 'severity_map', @@ -33,7 +33,7 @@ export const getVulnerabilitiesByResourceColumnsGrid = ( ): EuiDataGridColumn[] => [ { ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.resource_id, + id: vulnerabilitiesByResourceColumns.resourceId, displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceId', { defaultMessage: 'Resource ID', }), @@ -41,7 +41,7 @@ export const getVulnerabilitiesByResourceColumnsGrid = ( }, { ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.resource_name, + id: vulnerabilitiesByResourceColumns.resourceName, displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceName', { defaultMessage: 'Resource Name', }), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx new file mode 100644 index 0000000000000..4773e080dce47 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { HttpSetup } from '@kbn/core/public'; +import { Vulnerability } from '../../../../common/schemas'; +import { DetectionRuleCounter } from '../../../components/detection_rule_counter'; +import { createDetectionRuleFromVulnerabilityFinding } from '../utils/create_detection_rule_from_vulnerability'; + +const CNVM_TAG = 'CNVM'; + +export const VulnerabilityDetectionRuleCounter = ({ + vulnerability, +}: { + vulnerability: Vulnerability; +}) => { + const createVulnerabilityRuleFn = async (http: HttpSetup) => + await createDetectionRuleFromVulnerabilityFinding(http, vulnerability); + + return ( + + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx index 8d4e4eb3ea34a..516a25586b0ac 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx @@ -39,7 +39,7 @@ describe('', () => { getByText(mockVulnerabilityHit.vulnerability.description); const descriptionList = getByTestId(FINDINGS_VULNERABILITY_FLYOUT_DESCRIPTION_LIST); expect(descriptionList.textContent).toEqual( - `Resource:${mockVulnerabilityHit.resource?.name}Package:${mockVulnerabilityHit.vulnerability.package.name}Version:${mockVulnerabilityHit.vulnerability.package.version}` + `Resource ID:${mockVulnerabilityHit.resource?.id}Resource Name:${mockVulnerabilityHit.resource?.name}Package:${mockVulnerabilityHit.vulnerability.package.name}Version:${mockVulnerabilityHit.vulnerability.package.version}` ); getByText(mockVulnerabilityHit.vulnerability.severity); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx index 26aaf9926c0a5..ac2212c208dad 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx @@ -50,10 +50,17 @@ const getFlyoutDescriptionList = ( vulnerabilityRecord: CspVulnerabilityFinding ): EuiDescriptionListProps['listItems'] => [ + vulnerabilityRecord.resource?.id && { + title: i18n.translate( + 'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceId', + { defaultMessage: 'Resource ID' } + ), + description: vulnerabilityRecord.resource.id, + }, vulnerabilityRecord.resource?.name && { title: i18n.translate( - 'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle', - { defaultMessage: 'Resource' } + 'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceName', + { defaultMessage: 'Resource Name' } ), description: vulnerabilityRecord.resource.name, }, @@ -151,10 +158,10 @@ export const VulnerabilityFindingFlyout = ({ { defaultMessage: 'Loading' } ); - const vulnerabilityReference = getVulnerabilityReferenceUrl(vulnerabilityRecord); + const vulnerabilityReference = getVulnerabilityReferenceUrl(vulnerabilityRecord.vulnerability); const createVulnerabilityRuleFn = async (http: HttpSetup) => - await createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityRecord); + await createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityRecord.vulnerability); return ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx index 11cb395879b14..b25b2ae982d0a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx @@ -27,6 +27,7 @@ import { CVSScoreProps, Vendor } from '../types'; import { getVectorScoreList } from '../utils/get_vector_score_list'; import { OVERVIEW_TAB_VULNERABILITY_FLYOUT } from '../test_subjects'; import redhatLogo from '../../../assets/icons/redhat_logo.svg'; +import { VulnerabilityDetectionRuleCounter } from './vulnerability_detection_rule_counter'; const cvssVendors: Record = { nvd: 'NVD', @@ -239,7 +240,15 @@ export const VulnerabilityOverviewTab = ({ vulnerability }: VulnerabilityTabProp - + +

    + +

    + +

    ({ @@ -61,9 +62,17 @@ export const getVulnerabilitiesColumnsGrid = ( }, { ...defaultColumnProps(), - id: vulnerabilitiesColumns.resource, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resource', { - defaultMessage: 'Resource', + id: vulnerabilitiesColumns.resourceId, + displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resourceId', { + defaultMessage: 'Resource ID', + }), + cellActions, + }, + { + ...defaultColumnProps(), + id: vulnerabilitiesColumns.resourceName, + displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resourceName', { + defaultMessage: 'Resource Name', }), cellActions, }, @@ -95,7 +104,7 @@ export const getVulnerabilitiesColumnsGrid = ( }, { ...defaultColumnProps(), - id: vulnerabilitiesColumns.fix_version, + id: vulnerabilitiesColumns.fixedVersion, displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.fixVersion', { defaultMessage: 'Fix Version', }), diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/alert_stats_collector.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/alert_stats_collector.ts new file mode 100644 index 0000000000000..7e63af4fb1320 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/alert_stats_collector.ts @@ -0,0 +1,210 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { CloudSecurityAlertsStats } from './types'; +import { DETECTION_ENGINE_ALERTS_INDEX_DEFAULT } from '../../../../common/constants'; + +interface AlertsStats { + aggregations: { + cspm: { + rules_count: { + value: number; + }; + alerts_open: { + doc_count: number; + }; + alerts_acknowledged: { + doc_count: number; + }; + alerts_closed: { + doc_count: number; + }; + }; + kspm: { + rules_count: { + value: number; + }; + alerts_open: { + doc_count: number; + }; + alerts_acknowledged: { + doc_count: number; + }; + alerts_closed: { + doc_count: number; + }; + }; + vuln_mgmt: { + rules_count: { + value: number; + }; + alerts_open: { + doc_count: number; + }; + alerts_acknowledged: { + doc_count: number; + }; + alerts_closed: { + doc_count: number; + }; + }; + }; +} + +const getAlertsStatsQuery = (index: string) => ({ + size: 0, + query: { + bool: { + filter: [{ term: { 'kibana.alert.rule.tags': 'Cloud Security' } }], + }, + }, + sort: '@timestamp:desc', + index, + aggs: { + cspm: { + filter: { + term: { + 'kibana.alert.rule.tags': 'CSPM', + }, + }, + aggs: { + rules_count: { + cardinality: { + field: 'kibana.alert.rule.uuid', + }, + }, + alerts_open: { + filter: { + term: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + alerts_acknowledged: { + filter: { + term: { + 'kibana.alert.workflow_status': 'acknowledged', + }, + }, + }, + alerts_closed: { + filter: { + term: { + 'kibana.alert.workflow_status': 'closed', + }, + }, + }, + }, + }, + kspm: { + filter: { + term: { + 'kibana.alert.rule.tags': 'KSPM', + }, + }, + aggs: { + rules_count: { + cardinality: { + field: 'kibana.alert.rule.uuid', + }, + }, + alerts_open: { + filter: { + term: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + alerts_acknowledged: { + filter: { + term: { + 'kibana.alert.workflow_status': 'acknowledged', + }, + }, + }, + alerts_closed: { + filter: { + term: { + 'kibana.alert.workflow_status': 'closed', + }, + }, + }, + }, + }, + vuln_mgmt: { + filter: { + term: { + 'kibana.alert.rule.tags': 'CNVM', + }, + }, + aggs: { + rules_count: { + cardinality: { + field: 'kibana.alert.rule.uuid', + }, + }, + alerts_open: { + filter: { + term: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + alerts_acknowledged: { + filter: { + term: { + 'kibana.alert.workflow_status': 'acknowledged', + }, + }, + }, + alerts_closed: { + filter: { + term: { + 'kibana.alert.workflow_status': 'closed', + }, + }, + }, + }, + }, + }, +}); + +export const getAlertsStats = async ( + esClient: ElasticsearchClient, + logger: Logger +): Promise => { + const index = DETECTION_ENGINE_ALERTS_INDEX_DEFAULT; + + try { + const isIndexExists = await esClient.indices.exists({ + index, + }); + + if (isIndexExists) { + const alertsStats = await esClient.search(getAlertsStatsQuery(index)); + + const postureTypes = ['cspm', 'kspm', 'vuln_mgmt'] as const; + + return postureTypes.map((postureType) => ({ + posture_type: postureType, + rules_count: alertsStats.aggregations?.aggregations[postureType].rules_count.value, + alerts_count: alertsStats.aggregations?.aggregations[postureType].alerts_open.doc_count, + alerts_open_count: + alertsStats.aggregations?.aggregations[postureType].alerts_open.doc_count, + alerts_acknowledged_count: + alertsStats.aggregations?.aggregations[postureType].alerts_acknowledged.doc_count, + alerts_closed_count: + alertsStats.aggregations?.aggregations[postureType].alerts_closed.doc_count, + })) as CloudSecurityAlertsStats[]; + } + return []; + } catch (e) { + logger.error(`Failed to get index stats for ${index}: ${e}`); + return []; + } +}; diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts index 1b9b4f0370f6b..c9495c03eccdb 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts @@ -15,6 +15,7 @@ import { CspmUsage } from './types'; import { getAccountsStats } from './accounts_stats_collector'; import { getRulesStats } from './rules_stats_collector'; import { getInstallationStats } from './installation_stats_collector'; +import { getAlertsStats } from './alert_stats_collector'; export function registerCspmUsageCollector( logger: Logger, @@ -34,24 +35,31 @@ export function registerCspmUsageCollector( return true; }, fetch: async (collectorFetchContext: CollectorFetchContext) => { - const [indicesStats, accountsStats, resourcesStats, rulesStats, installationStats] = - await Promise.all([ - getIndicesStats( - collectorFetchContext.esClient, - collectorFetchContext.soClient, - coreServices, - logger - ), - getAccountsStats(collectorFetchContext.esClient, logger), - getResourcesStats(collectorFetchContext.esClient, logger), - getRulesStats(collectorFetchContext.esClient, logger), - getInstallationStats( - collectorFetchContext.esClient, - collectorFetchContext.soClient, - coreServices, - logger - ), - ]); + const [ + indicesStats, + accountsStats, + resourcesStats, + rulesStats, + installationStats, + alertsStats, + ] = await Promise.all([ + getIndicesStats( + collectorFetchContext.esClient, + collectorFetchContext.soClient, + coreServices, + logger + ), + getAccountsStats(collectorFetchContext.esClient, logger), + getResourcesStats(collectorFetchContext.esClient, logger), + getRulesStats(collectorFetchContext.esClient, logger), + getInstallationStats( + collectorFetchContext.esClient, + collectorFetchContext.soClient, + coreServices, + logger + ), + getAlertsStats(collectorFetchContext.esClient, logger), + ]); return { indices: indicesStats, @@ -59,6 +67,7 @@ export function registerCspmUsageCollector( resources_stats: resourcesStats, rules_stats: rulesStats, installation_stats: installationStats, + alerts_stats: alertsStats, }; }, schema: cspmUsageSchema, diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts index 578f2c17894df..5441992618192 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts @@ -156,4 +156,15 @@ export const cspmUsageSchema: MakeSchemaFrom = { account_type: { type: 'keyword' }, }, }, + alerts_stats: { + type: 'array', + items: { + posture_type: { type: 'keyword' }, + rules_count: { type: 'long' }, + alerts_count: { type: 'long' }, + alerts_open_count: { type: 'long' }, + alerts_closed_count: { type: 'long' }, + alerts_acknowledged_count: { type: 'long' }, + }, + }, }; diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts index 53a94067ed67a..0c04de498509a 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts @@ -13,6 +13,7 @@ export interface CspmUsage { accounts_stats: CspmAccountsStats[]; rules_stats: CspmRulesStats[]; installation_stats: CloudSecurityInstallationStats[]; + alerts_stats: CloudSecurityAlertsStats[]; } export interface PackageSetupStatus { @@ -88,3 +89,12 @@ export interface CloudSecurityInstallationStats { agent_count: number; account_type?: 'single-account' | 'organization-account'; } + +export interface CloudSecurityAlertsStats { + posture_type: string; + rules_count: number; + alerts_count: number; + alerts_open_count: number; + alerts_closed_count: number; + alerts_acknowledged_count: number; +} diff --git a/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts b/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts new file mode 100644 index 0000000000000..d464563155023 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { schema } from '@kbn/config-schema'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { + DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + GET_DETECTION_RULE_ALERTS_STATUS_PATH, +} from '../../../common/constants'; +import { CspRouter } from '../../types'; + +export interface VulnerabilitiesStatisticsQueryResult { + total: number; +} + +const DEFAULT_ALERTS_INDEX = '.alerts-security.alerts-default' as const; + +export const getDetectionEngineAlertsCountByRuleTags = async ( + esClient: ElasticsearchClient, + tags: string[] +) => { + return await esClient.search({ + size: 0, + query: { + bool: { + filter: [ + { term: { 'kibana.alert.rule.tags': 'Cloud Security' } }, + ...tags.map((tag) => ({ term: { 'kibana.alert.rule.tags': tag } })), + ], + }, + }, + sort: '@timestamp:desc', + index: DEFAULT_ALERTS_INDEX, + }); +}; + +const getDetectionEngineAlertsStatus = async (esClient: ElasticsearchClient, tags: string[]) => { + const alertsCountByTags = await getDetectionEngineAlertsCountByRuleTags(esClient, tags); + + const total = + typeof alertsCountByTags.hits.total === 'number' + ? alertsCountByTags.hits.total + : alertsCountByTags.hits.total?.value; + + return { + total, + }; +}; +export const defineGetDetectionEngineAlertsStatus = (router: CspRouter) => + router.versioned + .get({ + access: 'internal', + path: GET_DETECTION_RULE_ALERTS_STATUS_PATH, + }) + .addVersion( + { + version: DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + validate: { + request: { + query: schema.object({ + tags: schema.arrayOf(schema.string()), + }), + }, + }, + }, + async (context, request, response) => { + if (!(await context.fleet).authz.fleet.all) { + return response.forbidden(); + } + + const requestBody = request.query; + const cspContext = await context.csp; + + try { + const alerts = await getDetectionEngineAlertsStatus( + cspContext.esClient.asCurrentUser, + requestBody.tags + ); + return response.ok({ body: alerts }); + } catch (err) { + const error = transformError(err); + cspContext.logger.error(`Failed to fetch csp rules templates ${err}`); + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode, + }); + } + } + ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts index 426385682180d..a0e33bce73d3f 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts @@ -18,12 +18,13 @@ import { defineGetVulnerabilitiesDashboardRoute } from './vulnerabilities_dashbo import { defineGetBenchmarksRoute } from './benchmarks/benchmarks'; import { defineGetCspStatusRoute } from './status/status'; import { defineFindCspRuleTemplateRoute } from './csp_rule_template/get_csp_rule_template'; +import { defineGetDetectionEngineAlertsStatus } from './detection_engine/get_detection_engine_alerts_count_by_rule_tags'; /** * 1. Registers routes * 2. Registers routes handler context */ -export function setupRoutes({ +export async function setupRoutes({ core, logger, isPluginInitialized, @@ -38,6 +39,7 @@ export function setupRoutes({ defineGetBenchmarksRoute(router); defineGetCspStatusRoute(router); defineFindCspRuleTemplateRoute(router); + defineGetDetectionEngineAlertsStatus(router); core.http.registerRouteHandlerContext( PLUGIN_ID, diff --git a/x-pack/plugins/enterprise_search/common/connectors/connectors.ts b/x-pack/plugins/enterprise_search/common/connectors/connectors.ts index a1d5b87d9f965..3595ca7644770 100644 --- a/x-pack/plugins/enterprise_search/common/connectors/connectors.ts +++ b/x-pack/plugins/enterprise_search/common/connectors/connectors.ts @@ -38,6 +38,17 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'confluence', }, + { + iconPath: 'dropbox.svg', + isBeta: true, + isNative: true, + isTechPreview: false, + keywords: ['dropbox', 'connector'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.dropbox.name', { + defaultMessage: 'Dropbox', + }), + serviceType: 'dropbox', + }, { iconPath: 'jira_cloud.svg', isBeta: true, @@ -128,6 +139,17 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'postgresql', }, + { + iconPath: 'servicenow.svg', + isBeta: true, + isNative: true, + isTechPreview: false, + keywords: ['servicenow', 'cloud', 'connector'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.serviceNow.name', { + defaultMessage: 'ServiceNow', + }), + serviceType: 'servicenow', + }, { iconPath: 'sharepoint_online.svg', isBeta: false, @@ -139,17 +161,6 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'sharepoint_online', }, - { - iconPath: 'dropbox.svg', - isBeta: true, - isNative: true, - isTechPreview: false, - keywords: ['dropbox', 'connector'], - name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.dropbox.name', { - defaultMessage: 'Dropbox', - }), - serviceType: 'dropbox', - }, { iconPath: 'gmail.svg', isBeta: false, @@ -171,6 +182,16 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'oracle', }, + { + iconPath: 'onedrive.svg', + isBeta: true, + isNative: false, + keywords: ['network', 'drive', 'file', 'connector'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.oneDrive.name', { + defaultMessage: 'OneDrive', + }), + serviceType: 'onedrive', + }, { iconPath: 's3.svg', isBeta: true, @@ -181,17 +202,6 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 's3', }, - { - iconPath: 'servicenow.svg', - isBeta: true, - isNative: true, - isTechPreview: false, - keywords: ['servicenow', 'cloud', 'connector'], - name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.serviceNow.name', { - defaultMessage: 'ServiceNow', - }), - serviceType: 'servicenow', - }, { iconPath: 'slack.svg', isBeta: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx index 1d207db7b077c..1afc443077e15 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx @@ -55,28 +55,37 @@ import { ConnectorCheckable } from './connector_checkable'; export const SelectConnector: React.FC = () => { const { search } = useLocation(); + const { isCloud } = useValues(KibanaLogic); + const { hasPlatinumLicense } = useValues(LicensingLogic); + const hasNativeAccess = isCloud; const { service_type: serviceType } = parseQueryParams(search); const [useNativeFilter, setUseNativeFilter] = useState(false); const [useNonGAFilter, setUseNonGAFilter] = useState(true); const [searchTerm, setSearchTerm] = useState(''); - const filteredConnectors = useMemo( - () => - CONNECTORS.filter((connector) => + const filteredConnectors = useMemo(() => { + const nativeConnectors = hasNativeAccess + ? CONNECTORS.filter((connector) => connector.isNative).sort((a, b) => + a.name.localeCompare(b.name) + ) + : []; + const nonNativeConnectors = hasNativeAccess + ? CONNECTORS.filter((connector) => !connector.isNative).sort((a, b) => + a.name.localeCompare(b.name) + ) + : CONNECTORS.sort((a, b) => a.name.localeCompare(b.name)); + const connectors = [...nativeConnectors, ...nonNativeConnectors]; + return connectors + .filter((connector) => useNonGAFilter ? true : !connector.isBeta && !connector.isTechPreview ) - .filter((connector) => (useNativeFilter ? connector.isNative : true)) - .filter((connector) => - searchTerm ? connector.name.toLowerCase().includes(searchTerm.toLowerCase()) : true - ), - [useNonGAFilter, useNativeFilter, searchTerm] - ); + .filter((connector) => (useNativeFilter ? connector.isNative : true)) + .filter((connector) => + searchTerm ? connector.name.toLowerCase().includes(searchTerm.toLowerCase()) : true + ); + }, [useNonGAFilter, useNativeFilter, searchTerm]); const [selectedConnector, setSelectedConnector] = useState( Array.isArray(serviceType) ? serviceType[0] : serviceType ?? null ); - const { isCloud } = useValues(KibanaLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); - - const hasNativeAccess = isCloud; return ( = { externalDocsUrl: '', icon: CONNECTOR_ICONS.dropbox, }, + github: { + docsUrl: docLinks.connectorsGithub, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.github, + }, gmail: { - docsUrl: '', // TODO + docsUrl: docLinks.connectorsGmail, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.gmail, @@ -85,6 +91,12 @@ export const CONNECTORS_DICT: Record = { externalDocsUrl: '', icon: CONNECTOR_ICONS.network_drive, }, + onedrive: { + docsUrl: docLinks.connectorsOneDrive, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.onedrive, + }, oracle: { docsUrl: docLinks.connectorsOracle, externalAuthDocsUrl: @@ -104,18 +116,24 @@ export const CONNECTORS_DICT: Record = { externalDocsUrl: '', icon: CONNECTOR_ICONS.amazon_s3, }, + salesforce: { + docsUrl: docLinks.connectorsSalesforce, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.salesforce, + }, servicenow: { docsUrl: docLinks.connectorsServiceNow, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.servicenow, }, - sharepoint: { + sharepoint_server: { docsUrl: docLinks.connectorsSharepoint, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.sharepoint, - platinumOnly: true, + platinumOnly: false, }, sharepoint_online: { docsUrl: docLinks.connectorsSharepointOnline, @@ -125,11 +143,11 @@ export const CONNECTORS_DICT: Record = { platinumOnly: true, }, slack: { - docsUrl: '', // TODO + docsUrl: docLinks.connectorsSlack, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.slack, - platinumOnly: true, + platinumOnly: false, }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index a30c968077ace..377d2f9ccd71d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -67,6 +67,8 @@ class DocLinks { public connectorsConfluence: string; public connectorsContentExtraction: string; public connectorsDropbox: string; + public connectorsGithub: string; + public connectorsGmail: string; public connectorsGoogleCloudStorage: string; public connectorsGoogleDrive: string; public connectorsJira: string; @@ -75,12 +77,15 @@ class DocLinks { public connectorsMySQL: string; public connectorsNative: string; public connectorsNetworkDrive: string; + public connectorsOneDrive: string; public connectorsOracle: string; public connectorsPostgreSQL: string; public connectorsS3: string; + public connectorsSalesforce: string; public connectorsServiceNow: string; public connectorsSharepoint: string; public connectorsSharepointOnline: string; + public connectorsSlack: string; public connectorsWorkplaceSearch: string; public consoleGuide: string; public crawlerExtractionRules: string; @@ -226,6 +231,8 @@ class DocLinks { this.connectorsContentExtraction = ''; this.connectorsClients = ''; this.connectorsDropbox = ''; + this.connectorsGithub = ''; + this.connectorsGmail = ''; this.connectorsGoogleCloudStorage = ''; this.connectorsGoogleDrive = ''; this.connectorsJira = ''; @@ -234,12 +241,15 @@ class DocLinks { this.connectorsMySQL = ''; this.connectorsNative = ''; this.connectorsNetworkDrive = ''; + this.connectorsOneDrive = ''; this.connectorsOracle = ''; this.connectorsPostgreSQL = ''; this.connectorsS3 = ''; + this.connectorsSalesforce = ''; this.connectorsServiceNow = ''; this.connectorsSharepoint = ''; this.connectorsSharepointOnline = ''; + this.connectorsSlack = ''; this.connectorsWorkplaceSearch = ''; this.consoleGuide = ''; this.crawlerExtractionRules = ''; @@ -386,9 +396,11 @@ class DocLinks { this.connectorsContentExtraction = docLinks.links.enterpriseSearch.connectorsContentExtraction; this.connectorsClients = docLinks.links.enterpriseSearch.connectorsClients; this.connectorsDropbox = docLinks.links.enterpriseSearch.connectorsDropbox; + this.connectorsGithub = docLinks.links.enterpriseSearch.connectorsGithub; this.connectorsGoogleCloudStorage = docLinks.links.enterpriseSearch.connectorsGoogleCloudStorage; this.connectorsGoogleDrive = docLinks.links.enterpriseSearch.connectorsGoogleDrive; + this.connectorsGmail = docLinks.links.enterpriseSearch.connectorsGmail; this.connectorsJira = docLinks.links.enterpriseSearch.connectorsJira; this.connectorsMicrosoftSQL = docLinks.links.enterpriseSearch.connectorsMicrosoftSQL; this.connectorsMongoDB = docLinks.links.enterpriseSearch.connectorsMongoDB; @@ -401,6 +413,7 @@ class DocLinks { this.connectorsServiceNow = docLinks.links.enterpriseSearch.connectorsServiceNow; this.connectorsSharepoint = docLinks.links.enterpriseSearch.connectorsSharepoint; this.connectorsSharepointOnline = docLinks.links.enterpriseSearch.connectorsSharepointOnline; + this.connectorsSlack = docLinks.links.enterpriseSearch.connectorsSlack; this.connectorsWorkplaceSearch = docLinks.links.enterpriseSearch.connectorsWorkplaceSearch; this.consoleGuide = docLinks.links.console.guide; this.crawlerExtractionRules = docLinks.links.enterpriseSearch.crawlerExtractionRules; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts index 0f41254093984..ab3dc7a6cfb37 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts @@ -18,9 +18,11 @@ import mongodb from '../../../assets/source_icons/mongodb.svg'; import microsoft_sql from '../../../assets/source_icons/mssql.svg'; import mysql from '../../../assets/source_icons/mysql.svg'; import network_drive from '../../../assets/source_icons/network_drive.svg'; +import onedrive from '../../../assets/source_icons/onedrive.svg'; import oracle from '../../../assets/source_icons/oracle.svg'; import postgresql from '../../../assets/source_icons/postgresql.svg'; import amazon_s3 from '../../../assets/source_icons/s3.svg'; +import salesforce from '../../../assets/source_icons/salesforce.svg'; import servicenow from '../../../assets/source_icons/servicenow.svg'; import sharepoint from '../../../assets/source_icons/sharepoint.svg'; import sharepoint_online from '../../../assets/source_icons/sharepoint_online.svg'; @@ -41,8 +43,10 @@ export const CONNECTOR_ICONS = { mongodb, mysql, network_drive, + onedrive, oracle, postgresql, + salesforce, servicenow, sharepoint, sharepoint_online, diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index 6d2e67d602548..cedbd4215a656 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -34,36 +34,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['enterprise_search', 'workplace_search_content_source'], }, - { - id: 'onedrive', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName', { - defaultMessage: 'OneDrive', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription', - { - defaultMessage: 'Search over your files stored on OneDrive with Workplace Search.', - } - ), - categories: ['enterprise_search', 'azure', 'workplace_search_content_source'], - uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/one_drive', - }, - { - id: 'salesforce_sandbox', - title: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName', - { - defaultMessage: 'Salesforce Sandbox', - } - ), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription', - { - defaultMessage: 'Search over your content on Salesforce Sandbox with Workplace Search.', - } - ), - categories: ['enterprise_search', 'workplace_search_content_source'], - }, { id: 'zendesk', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName', { @@ -359,6 +329,40 @@ export const registerEnterpriseSearchIntegrations = ( shipper: 'enterprise_search', isBeta: false, }); + + customIntegrations.registerCustomIntegration({ + id: 'onedrive', + title: i18n.translate('xpack.enterpriseSearch.integrations.oneDriveTitle', { + defaultMessage: 'OneDrive', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.oneDriveDescription', + { + defaultMessage: 'Search over your content on OneDrive.', + } + ), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'datastore', + 'connector', + 'connector_client', + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=salesforce', + icons: [ + { + type: 'svg', + src: http.basePath.prepend( + '/plugins/enterpriseSearch/assets/source_icons/salesforce_sandbox.svg' + ), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + customIntegrations.registerCustomIntegration({ id: 'build_a_connector', title: i18n.translate('xpack.enterpriseSearch.integrations.buildAConnectorName', { @@ -417,6 +421,39 @@ export const registerEnterpriseSearchIntegrations = ( isBeta: false, }); + customIntegrations.registerCustomIntegration({ + id: 'salesforce_sandbox', + title: i18n.translate('xpack.enterpriseSearch.integrations.salesforceSandboxTitle', { + defaultMessage: 'Salesforce Sandbox', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription', + { + defaultMessage: 'Search over your content on Salesforce Sandbox.', + } + ), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'datastore', + 'connector', + 'connector_client', + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=salesforce', + icons: [ + { + type: 'svg', + src: http.basePath.prepend( + '/plugins/enterpriseSearch/assets/source_icons/salesforce_sandbox.svg' + ), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + customIntegrations.registerCustomIntegration({ id: 'servicenow', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.serviceNowName', { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx index 7ed959e40efa0..0ca51d6f595fa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx @@ -58,7 +58,7 @@ export const PostInstallCloudFormationModal: React.FunctionComponent<{ - + {error && isError && ( <> diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx index 83031548293f7..61ed68a059cab 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx @@ -50,7 +50,9 @@ export const CloudFormationInstructions: React.FunctionComponent = ({ } )} > - + ); -export const CloudFormationGuide = () => { +export const CloudFormationGuide = ({ + awsAccountType, +}: { + awsAccountType?: CloudSecurityIntegrationAwsAccountType; +}) => { return (

    @@ -44,12 +50,21 @@ export const CloudFormationGuide = () => {

      -
    1. - -
    2. + {awsAccountType === 'organization-account' ? ( +
    3. + +
    4. + ) : ( +
    5. + +
    6. + )}
    7. { +describe('fleet usage telemetry', () => { let core: any; let esServer: TestElasticsearchUtils; let kbnServer: TestKibanaUtils; @@ -437,7 +435,11 @@ describe.skip('fleet usage telemetry', () => { }, ], components_status: [ - /* To uncomment when ES new snapshot will be built + { + id: 'beat/metrics-monitoring', + status: 'HEALTHY', + count: 2, + }, { id: 'filestream-monitoring', status: 'HEALTHY', @@ -448,11 +450,6 @@ describe.skip('fleet usage telemetry', () => { status: 'UNHEALTHY', count: 1, }, - { - id: 'beat/metrics-monitoring', - status: 'HEALTHY', - count: 2, - }, */ ], fleet_server_config: { policies: [ diff --git a/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts b/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts index fda66eefe3889..626ec8a26dfc2 100644 --- a/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts +++ b/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts @@ -8,53 +8,79 @@ import { calculateImpactEstimates } from '.'; describe('calculateImpactEstimates', () => { it('calculates impact when countExclusive is lower than countInclusive', () => { - expect( - calculateImpactEstimates({ - countExclusive: 500, - countInclusive: 1000, - totalSamples: 10000, - totalSeconds: 15 * 60, // 15m - }) - ).toEqual({ + const { selfCPU, totalCPU, totalSamples } = calculateImpactEstimates({ + countExclusive: 500, + countInclusive: 1000, + totalSamples: 10000, + totalSeconds: 15 * 60, // 15m + }); + + expect(totalCPU).toEqual({ annualizedCo2: 17.909333333333336, - annualizedCo2NoChildren: 8.954666666666668, annualizedCoreSeconds: 1752000, - annualizedCoreSecondsNoChildren: 876000, annualizedDollarCost: 20.683333333333334, - annualizedDollarCostNoChildren: 10.341666666666667, co2: 0.0005111111111111112, - co2NoChildren: 0.0002555555555555556, coreSeconds: 50, - coreSecondsNoChildren: 25, dollarCost: 0.0005902777777777778, - dollarCostNoChildren: 0.0002951388888888889, percentage: 0.1, - percentageNoChildren: 0.05, + }); + + expect(selfCPU).toEqual({ + annualizedCo2: 8.954666666666668, + annualizedCoreSeconds: 876000, + annualizedDollarCost: 10.341666666666667, + co2: 0.0002555555555555556, + coreSeconds: 25, + dollarCost: 0.0002951388888888889, + percentage: 0.05, + }); + + expect(totalSamples).toEqual({ + percentage: 1, + coreSeconds: 500, + annualizedCoreSeconds: 17520000, + co2: 0.005111111111111111, + annualizedCo2: 179.09333333333333, + dollarCost: 0.0059027777777777785, + annualizedDollarCost: 206.83333333333337, }); }); it('calculates impact', () => { - expect( - calculateImpactEstimates({ - countExclusive: 1000, - countInclusive: 1000, - totalSamples: 10000, - totalSeconds: 15 * 60, // 15m - }) - ).toEqual({ + const { selfCPU, totalCPU, totalSamples } = calculateImpactEstimates({ + countExclusive: 1000, + countInclusive: 1000, + totalSamples: 10000, + totalSeconds: 15 * 60, // 15m + }); + + expect(totalCPU).toEqual({ annualizedCo2: 17.909333333333336, - annualizedCo2NoChildren: 17.909333333333336, annualizedCoreSeconds: 1752000, - annualizedCoreSecondsNoChildren: 1752000, annualizedDollarCost: 20.683333333333334, - annualizedDollarCostNoChildren: 20.683333333333334, co2: 0.0005111111111111112, - co2NoChildren: 0.0005111111111111112, coreSeconds: 50, - coreSecondsNoChildren: 50, dollarCost: 0.0005902777777777778, - dollarCostNoChildren: 0.0005902777777777778, percentage: 0.1, - percentageNoChildren: 0.1, + }); + + expect(selfCPU).toEqual({ + annualizedCo2: 17.909333333333336, + annualizedCoreSeconds: 1752000, + annualizedDollarCost: 20.683333333333334, + co2: 0.0005111111111111112, + coreSeconds: 50, + dollarCost: 0.0005902777777777778, + percentage: 0.1, + }); + + expect(totalSamples).toEqual({ + percentage: 1, + coreSeconds: 500, + annualizedCoreSeconds: 17520000, + co2: 0.005111111111111111, + annualizedCo2: 179.09333333333333, + dollarCost: 0.0059027777777777785, + annualizedDollarCost: 206.83333333333337, }); }); }); diff --git a/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts b/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts index e7799fd9acde1..70cfdc109a107 100644 --- a/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts +++ b/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts @@ -27,40 +27,52 @@ export function calculateImpactEstimates({ totalSamples: number; totalSeconds: number; }) { - const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds; + return { + totalSamples: calculateImpact({ + samples: totalSamples, + totalSamples, + totalSeconds, + }), + totalCPU: calculateImpact({ + samples: countInclusive, + totalSamples, + totalSeconds, + }), + selfCPU: calculateImpact({ + samples: countExclusive, + totalSamples, + totalSeconds, + }), + }; +} - const percentage = countInclusive / totalSamples; - const percentageNoChildren = countExclusive / totalSamples; +function calculateImpact({ + samples, + totalSamples, + totalSeconds, +}: { + samples: number; + totalSamples: number; + totalSeconds: number; +}) { + const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds; const totalCoreSeconds = totalSamples / 20; + const percentage = samples / totalSamples; const coreSeconds = totalCoreSeconds * percentage; const annualizedCoreSeconds = coreSeconds * annualizedScaleUp; - const coreSecondsNoChildren = totalCoreSeconds * percentageNoChildren; - const annualizedCoreSecondsNoChildren = coreSecondsNoChildren * annualizedScaleUp; const coreHours = coreSeconds / (60 * 60); - const coreHoursNoChildren = coreSecondsNoChildren / (60 * 60); const co2 = ((PER_CORE_WATT * coreHours) / 1000.0) * CO2_PER_KWH; - const co2NoChildren = ((PER_CORE_WATT * coreHoursNoChildren) / 1000.0) * CO2_PER_KWH; const annualizedCo2 = co2 * annualizedScaleUp; - const annualizedCo2NoChildren = co2NoChildren * annualizedScaleUp; const dollarCost = coreHours * CORE_COST_PER_HOUR; const annualizedDollarCost = dollarCost * annualizedScaleUp; - const dollarCostNoChildren = coreHoursNoChildren * CORE_COST_PER_HOUR; - const annualizedDollarCostNoChildren = dollarCostNoChildren * annualizedScaleUp; return { percentage, - percentageNoChildren, coreSeconds, annualizedCoreSeconds, - coreSecondsNoChildren, - annualizedCoreSecondsNoChildren, co2, - co2NoChildren, annualizedCo2, - annualizedCo2NoChildren, dollarCost, annualizedDollarCost, - dollarCostNoChildren, - annualizedDollarCostNoChildren, }; } diff --git a/x-pack/plugins/profiling/common/functions.test.ts b/x-pack/plugins/profiling/common/functions.test.ts index 10578dab72b51..1ba31d397a338 100644 --- a/x-pack/plugins/profiling/common/functions.test.ts +++ b/x-pack/plugins/profiling/common/functions.test.ts @@ -17,7 +17,7 @@ describe('TopN function operations', () => { const { events, stackTraces, stackFrames, executables, samplingRate } = decodeStackTraceResponse(response); - describe(`stacktraces from ${seconds} seconds and upsampled by ${upsampledBy}`, () => { + describe(`stacktraces upsampled by ${upsampledBy}`, () => { const maxTopN = 5; const topNFunctions = createTopNFunctions({ events, @@ -27,7 +27,6 @@ describe('TopN function operations', () => { startIndex: 0, endIndex: maxTopN, samplingRate, - totalSeconds: seconds, }); const exclusiveCounts = topNFunctions.TopN.map((value) => value.CountExclusive); diff --git a/x-pack/plugins/profiling/common/functions.ts b/x-pack/plugins/profiling/common/functions.ts index caff616d1951c..304c56b81e906 100644 --- a/x-pack/plugins/profiling/common/functions.ts +++ b/x-pack/plugins/profiling/common/functions.ts @@ -6,7 +6,6 @@ */ import * as t from 'io-ts'; import { sumBy } from 'lodash'; -import { calculateImpactEstimates } from './calculate_impact_estimates'; import { createFrameGroupID, FrameGroupID } from './frame_group'; import { createStackFrameMetadata, @@ -35,18 +34,14 @@ type TopNFunction = Pick< > & { Id: string; Rank: number; - impactEstimates?: ReturnType; - selfCPUPerc: number; - totalCPUPerc: number; }; export interface TopNFunctions { TotalCount: number; TopN: TopNFunction[]; SamplingRate: number; - impactEstimates?: ReturnType; - selfCPUPerc: number; - totalCPUPerc: number; + selfCPU: number; + totalCPU: number; } export function createTopNFunctions({ @@ -57,7 +52,6 @@ export function createTopNFunctions({ stackFrames, stackTraces, startIndex, - totalSeconds, }: { endIndex: number; events: Map; @@ -66,7 +60,6 @@ export function createTopNFunctions({ stackFrames: Map; stackTraces: Map; startIndex: number; - totalSeconds: number; }): TopNFunctions { // The `count` associated with a frame provides the total number of // traces in which that node has appeared at least once. However, a @@ -167,52 +160,25 @@ export function createTopNFunctions({ const framesAndCountsAndIds = topN.slice(startIndex, endIndex).map((frameAndCount, i) => { const countExclusive = frameAndCount.CountExclusive; const countInclusive = frameAndCount.CountInclusive; - const totalCPUPerc = (countInclusive / totalCount) * 100; - const selfCPUPerc = (countExclusive / totalCount) * 100; - - const impactEstimates = - totalSeconds > 0 - ? calculateImpactEstimates({ - countExclusive, - countInclusive, - totalSamples: totalCount, - totalSeconds, - }) - : undefined; + return { Rank: i + 1, Frame: frameAndCount.Frame, CountExclusive: countExclusive, - selfCPUPerc, CountInclusive: countInclusive, - totalCPUPerc, Id: frameAndCount.FrameGroupID, - impactEstimates, }; }); const sumSelfCPU = sumBy(framesAndCountsAndIds, 'CountExclusive'); - const selfCPUPerc = (sumSelfCPU / totalCount) * 100; const sumTotalCPU = sumBy(framesAndCountsAndIds, 'CountInclusive'); - const totalCPUPerc = (sumTotalCPU / totalCount) * 100; - - const impactEstimates = - totalSeconds > 0 - ? calculateImpactEstimates({ - countExclusive: sumSelfCPU, - countInclusive: sumTotalCPU, - totalSamples: totalCount, - totalSeconds, - }) - : undefined; return { TotalCount: totalCount, TopN: framesAndCountsAndIds, SamplingRate: samplingRate, - impactEstimates, - selfCPUPerc, - totalCPUPerc, + selfCPU: sumSelfCPU, + totalCPU: sumTotalCPU, }; } diff --git a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx index 097997d970833..ce5835f57e7a7 100644 --- a/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx +++ b/x-pack/plugins/profiling/public/components/flamegraph/flamegraph_tooltip.tsx @@ -99,8 +99,8 @@ export function FlameGraphTooltip({ labelStyle={{ fontWeight: 'bold' }} /> } - value={impactEstimates.percentage} - comparison={comparisonImpactEstimates?.percentage} + value={impactEstimates.totalCPU.percentage} + comparison={comparisonImpactEstimates?.totalCPU.percentage} formatValue={asPercentage} showDifference formatDifferenceAsPercentage @@ -115,8 +115,8 @@ export function FlameGraphTooltip({ labelStyle={{ fontWeight: 'bold' }} /> } - value={impactEstimates.percentageNoChildren} - comparison={comparisonImpactEstimates?.percentageNoChildren} + value={impactEstimates.selfCPU.percentage} + comparison={comparisonImpactEstimates?.selfCPU.percentage} showDifference formatDifferenceAsPercentage formatValue={asPercentage} @@ -144,8 +144,8 @@ export function FlameGraphTooltip({ label={i18n.translate('xpack.profiling.flameGraphTooltip.annualizedCo2', { defaultMessage: `Annualized CO2`, })} - value={impactEstimates.annualizedCo2} - comparison={comparisonImpactEstimates?.annualizedCo2} + value={impactEstimates.totalCPU.annualizedCo2} + comparison={comparisonImpactEstimates?.totalCPU.annualizedCo2} formatValue={asWeight} showDifference formatDifferenceAsPercentage={false} @@ -155,8 +155,8 @@ export function FlameGraphTooltip({ label={i18n.translate('xpack.profiling.flameGraphTooltip.annualizedDollarCost', { defaultMessage: `Annualized dollar cost`, })} - value={impactEstimates.annualizedDollarCost} - comparison={comparisonImpactEstimates?.annualizedDollarCost} + value={impactEstimates.totalCPU.annualizedDollarCost} + comparison={comparisonImpactEstimates?.totalCPU.annualizedDollarCost} formatValue={asCost} showDifference formatDifferenceAsPercentage={false} diff --git a/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx index f2ab508e10c58..43e34b6f6901e 100644 --- a/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx +++ b/x-pack/plugins/profiling/public/components/frame_information_window/get_impact_rows.tsx @@ -28,22 +28,7 @@ export function getImpactRows({ totalSeconds: number; isApproximate: boolean; }) { - const { - percentage, - percentageNoChildren, - coreSeconds, - annualizedCoreSeconds, - coreSecondsNoChildren, - co2, - co2NoChildren, - annualizedCo2, - annualizedCo2NoChildren, - dollarCost, - dollarCostNoChildren, - annualizedDollarCost, - annualizedDollarCostNoChildren, - annualizedCoreSecondsNoChildren, - } = calculateImpactEstimates({ + const { selfCPU, totalCPU } = calculateImpactEstimates({ countInclusive, countExclusive, totalSamples, @@ -53,11 +38,11 @@ export function getImpactRows({ const impactRows = [ { label: , - value: asPercentage(percentage), + value: asPercentage(totalCPU.percentage), }, { label: , - value: asPercentage(percentageNoChildren), + value: asPercentage(selfCPU.percentage), }, { label: i18n.translate('xpack.profiling.flameGraphInformationWindow.samplesInclusiveLabel', { @@ -76,28 +61,28 @@ export function getImpactRows({ 'xpack.profiling.flameGraphInformationWindow.coreSecondsInclusiveLabel', { defaultMessage: 'Core-seconds' } ), - value: asDuration(coreSeconds), + value: asDuration(totalCPU.coreSeconds), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.coreSecondsExclusiveLabel', { defaultMessage: 'Core-seconds (excl. children)' } ), - value: asDuration(coreSecondsNoChildren), + value: asDuration(selfCPU.coreSeconds), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.annualizedCoreSecondsInclusiveLabel', { defaultMessage: 'Annualized core-seconds' } ), - value: asDuration(annualizedCoreSeconds), + value: asDuration(totalCPU.annualizedCoreSeconds), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.annualizedCoreSecondsExclusiveLabel', { defaultMessage: 'Annualized core-seconds (excl. children)' } ), - value: asDuration(annualizedCoreSecondsNoChildren), + value: asDuration(selfCPU.annualizedCoreSeconds), }, { label: i18n.translate( @@ -106,56 +91,56 @@ export function getImpactRows({ defaultMessage: 'CO2 emission', } ), - value: asWeight(co2), + value: asWeight(totalCPU.co2), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.co2EmissionExclusiveLabel', { defaultMessage: 'CO2 emission (excl. children)' } ), - value: asWeight(co2NoChildren), + value: asWeight(selfCPU.co2), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.annualizedCo2InclusiveLabel', { defaultMessage: 'Annualized CO2' } ), - value: asWeight(annualizedCo2), + value: asWeight(totalCPU.annualizedCo2), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.annualizedCo2ExclusiveLabel', { defaultMessage: 'Annualized CO2 (excl. children)' } ), - value: asWeight(annualizedCo2NoChildren), + value: asWeight(selfCPU.annualizedCo2), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.dollarCostInclusiveLabel', { defaultMessage: 'Dollar cost' } ), - value: asCost(dollarCost), + value: asCost(totalCPU.dollarCost), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.dollarCostExclusiveLabel', { defaultMessage: 'Dollar cost (excl. children)' } ), - value: asCost(dollarCostNoChildren), + value: asCost(selfCPU.dollarCost), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.annualizedDollarCostInclusiveLabel', { defaultMessage: 'Annualized dollar cost' } ), - value: asCost(annualizedDollarCost), + value: asCost(totalCPU.annualizedDollarCost), }, { label: i18n.translate( 'xpack.profiling.flameGraphInformationWindow.annualizedDollarCostExclusiveLabel', { defaultMessage: 'Annualized dollar cost (excl. children)' } ), - value: asCost(annualizedDollarCostNoChildren), + value: asCost(selfCPU.annualizedDollarCost), }, ]; diff --git a/x-pack/plugins/profiling/public/components/topn_functions/function_row.tsx b/x-pack/plugins/profiling/public/components/topn_functions/function_row.tsx index 53f4a3701125e..4aa4b836eac8a 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/function_row.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions/function_row.tsx @@ -64,10 +64,10 @@ export function FunctionRow({ return ( - {functionRow.diff.rank} + 0 ? 'sortUp' : 'sortDown'} color={color} /> - 0 ? 'sortDown' : 'sortUp'} color={color} /> + {Math.abs(functionRow.diff.rank)} ); @@ -102,16 +102,16 @@ export function FunctionRow({ if ( columnId === TopNFunctionSortField.AnnualizedCo2 && - functionRow.impactEstimates?.annualizedCo2 + functionRow.impactEstimates?.selfCPU?.annualizedCo2 ) { - return
      {asWeight(functionRow.impactEstimates.annualizedCo2)}
      ; + return
      {asWeight(functionRow.impactEstimates.selfCPU.annualizedCo2)}
      ; } if ( columnId === TopNFunctionSortField.AnnualizedDollarCost && - functionRow.impactEstimates?.annualizedDollarCost + functionRow.impactEstimates?.selfCPU?.annualizedDollarCost ) { - return
      {asCost(functionRow.impactEstimates.annualizedDollarCost)}
      ; + return
      {asCost(functionRow.impactEstimates.selfCPU.annualizedDollarCost)}
      ; } return null; diff --git a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx index 52aacc6e4ae7a..d3528b522443b 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx @@ -88,8 +88,15 @@ export const TopNFunctionsGrid = forwardRef( comparisonScaleFactor, comparisonTopNFunctions, topNFunctions, + totalSeconds, }); - }, [topNFunctions, comparisonTopNFunctions, comparisonScaleFactor, baselineScaleFactor]); + }, [ + baselineScaleFactor, + comparisonScaleFactor, + comparisonTopNFunctions, + topNFunctions, + totalSeconds, + ]); const { columns, leadingControlColumns } = useMemo(() => { const gridColumns: EuiDataGridColumn[] = [ diff --git a/x-pack/plugins/profiling/public/components/topn_functions/utils.test.ts b/x-pack/plugins/profiling/public/components/topn_functions/utils.test.ts index 98ed1943ecedb..f0c6ca1a25c8d 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/utils.test.ts +++ b/x-pack/plugins/profiling/public/components/topn_functions/utils.test.ts @@ -18,8 +18,8 @@ describe('Top N functions: Utils', () => { it('returns correct value when percentage is 0', () => { expect(getColorLabel(0)).toEqual({ - color: 'danger', - label: '<0.01', + color: 'text', + label: '0%', icon: undefined, }); }); @@ -31,5 +31,13 @@ describe('Top N functions: Utils', () => { icon: 'sortDown', }); }); + + it('returns correct value when percentage is Infinity', () => { + expect(getColorLabel(Infinity)).toEqual({ + color: 'text', + label: undefined, + icon: undefined, + }); + }); }); }); diff --git a/x-pack/plugins/profiling/public/components/topn_functions/utils.ts b/x-pack/plugins/profiling/public/components/topn_functions/utils.ts index bf192de6bd177..f44454d255601 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/utils.ts +++ b/x-pack/plugins/profiling/public/components/topn_functions/utils.ts @@ -10,12 +10,20 @@ import { StackFrameMetadata } from '../../../common/profiling'; import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates'; export function getColorLabel(percent: number) { + if (percent === 0) { + return { color: 'text', label: `0%`, icon: undefined }; + } + const color = percent < 0 ? 'success' : 'danger'; const icon = percent < 0 ? 'sortUp' : 'sortDown'; const isSmallPercent = Math.abs(percent) <= 0.01; - const label = isSmallPercent ? '<0.01' : Math.abs(percent).toFixed(2) + '%'; + const value = isSmallPercent ? '<0.01' : Math.abs(percent).toFixed(2); + + if (isFinite(percent)) { + return { color, label: `${value}%`, icon }; + } - return { color, label, icon: isSmallPercent ? undefined : icon }; + return { color: 'text', label: undefined, icon: undefined }; } export function scaleValue({ value, scaleFactor = 1 }: { value: number; scaleFactor?: number }) { @@ -47,11 +55,13 @@ export function getFunctionsRows({ comparisonScaleFactor, comparisonTopNFunctions, topNFunctions, + totalSeconds, }: { baselineScaleFactor?: number; comparisonScaleFactor?: number; comparisonTopNFunctions?: TopNFunctions; topNFunctions?: TopNFunctions; + totalSeconds: number; }): IFunctionRow[] { if (!topNFunctions || !topNFunctions.TotalCount || topNFunctions.TotalCount === 0) { return []; @@ -64,26 +74,43 @@ export function getFunctionsRows({ return topNFunctions.TopN.filter((topN) => topN.CountExclusive > 0).map((topN, i) => { const comparisonRow = comparisonDataById?.[topN.Id]; - const topNCountExclusiveScaled = scaleValue({ + const scaledSelfCPU = scaleValue({ value: topN.CountExclusive, scaleFactor: baselineScaleFactor, }); + const totalCPUPerc = (topN.CountInclusive / topNFunctions.TotalCount) * 100; + const selfCPUPerc = (topN.CountExclusive / topNFunctions.TotalCount) * 100; + + const impactEstimates = + totalSeconds > 0 + ? calculateImpactEstimates({ + countExclusive: topN.CountExclusive, + countInclusive: topN.CountInclusive, + totalSamples: topNFunctions.TotalCount, + totalSeconds, + }) + : undefined; + function calculateDiff() { if (comparisonTopNFunctions && comparisonRow) { - const comparisonCountExclusiveScaled = scaleValue({ + const comparisonScaledSelfCPU = scaleValue({ value: comparisonRow.CountExclusive, scaleFactor: comparisonScaleFactor, }); + const scaledDiffSamples = scaledSelfCPU - comparisonScaledSelfCPU; + return { rank: topN.Rank - comparisonRow.Rank, - samples: topNCountExclusiveScaled - comparisonCountExclusiveScaled, + samples: scaledDiffSamples, selfCPU: comparisonRow.CountExclusive, totalCPU: comparisonRow.CountInclusive, - selfCPUPerc: topN.selfCPUPerc - comparisonRow.selfCPUPerc, - totalCPUPerc: topN.totalCPUPerc - comparisonRow.totalCPUPerc, - impactEstimates: comparisonRow.impactEstimates, + selfCPUPerc: + selfCPUPerc - (comparisonRow.CountExclusive / comparisonTopNFunctions.TotalCount) * 100, + totalCPUPerc: + totalCPUPerc - + (comparisonRow.CountInclusive / comparisonTopNFunctions.TotalCount) * 100, }; } } @@ -91,12 +118,12 @@ export function getFunctionsRows({ return { rank: topN.Rank, frame: topN.Frame, - samples: topNCountExclusiveScaled, - selfCPUPerc: topN.selfCPUPerc, - totalCPUPerc: topN.totalCPUPerc, + samples: scaledSelfCPU, + selfCPUPerc, + totalCPUPerc, selfCPU: topN.CountExclusive, totalCPU: topN.CountInclusive, - impactEstimates: topN.impactEstimates, + impactEstimates, diff: calculateDiff(), }; }); diff --git a/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx index 1a89c93c55e9d..d2057bfafcdcc 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions_summary/index.tsx @@ -7,7 +7,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useMemo } from 'react'; +import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates'; import { TopNFunctions } from '../../../common/functions'; import { asCost } from '../../utils/formatters/as_cost'; import { asWeight } from '../../utils/formatters/as_weight'; @@ -20,6 +21,8 @@ interface Props { baselineScaleFactor?: number; comparisonScaleFactor?: number; isLoading: boolean; + baselineDuration: number; + comparisonDuration: number; } const ESTIMATED_VALUE_LABEL = i18n.translate('xpack.profiling.diffTopNFunctions.estimatedValue', { @@ -29,32 +32,65 @@ const ESTIMATED_VALUE_LABEL = i18n.translate('xpack.profiling.diffTopNFunctions. export function TopNFunctionsSummary({ baselineTopNFunctions, comparisonTopNFunctions, - baselineScaleFactor, - comparisonScaleFactor, + baselineScaleFactor = 1, + comparisonScaleFactor = 1, isLoading, + baselineDuration, + comparisonDuration, }: Props) { - const totalSamplesDiff = calculateBaseComparisonDiff({ - baselineValue: baselineTopNFunctions?.TotalCount || 0, - baselineScaleFactor, - comparisonValue: comparisonTopNFunctions?.TotalCount || 0, - comparisonScaleFactor, - }); + const baselineScaledTotalSamples = baselineTopNFunctions + ? baselineTopNFunctions.TotalCount * baselineScaleFactor + : 0; - const co2EmissionDiff = calculateBaseComparisonDiff({ - baselineValue: baselineTopNFunctions?.impactEstimates?.annualizedCo2 || 0, - baselineScaleFactor, - comparisonValue: comparisonTopNFunctions?.impactEstimates?.annualizedCo2 || 0, - comparisonScaleFactor, - formatValue: asWeight, - }); + const comparisonScaledTotalSamples = comparisonTopNFunctions + ? comparisonTopNFunctions.TotalCount * comparisonScaleFactor + : 0; - const costImpactDiff = calculateBaseComparisonDiff({ - baselineValue: baselineTopNFunctions?.impactEstimates?.annualizedDollarCost || 0, - baselineScaleFactor, - comparisonValue: comparisonTopNFunctions?.impactEstimates?.annualizedDollarCost || 0, - comparisonScaleFactor, - formatValue: asCost, - }); + const { co2EmissionDiff, costImpactDiff, totalSamplesDiff } = useMemo(() => { + const baseImpactEstimates = baselineTopNFunctions + ? // Do NOT scale values here. This is intended to show the exact values spent throughout the year + calculateImpactEstimates({ + countExclusive: baselineTopNFunctions.selfCPU, + countInclusive: baselineTopNFunctions.totalCPU, + totalSamples: baselineTopNFunctions.TotalCount, + totalSeconds: baselineDuration, + }) + : undefined; + + const comparisonImpactEstimates = comparisonTopNFunctions + ? // Do NOT scale values here. This is intended to show the exact values spent throughout the year + calculateImpactEstimates({ + countExclusive: comparisonTopNFunctions.selfCPU, + countInclusive: comparisonTopNFunctions.totalCPU, + totalSamples: comparisonTopNFunctions.TotalCount, + totalSeconds: comparisonDuration, + }) + : undefined; + + return { + totalSamplesDiff: calculateBaseComparisonDiff({ + baselineValue: baselineScaledTotalSamples || 0, + comparisonValue: comparisonScaledTotalSamples || 0, + }), + co2EmissionDiff: calculateBaseComparisonDiff({ + baselineValue: baseImpactEstimates?.totalSamples?.annualizedCo2 || 0, + comparisonValue: comparisonImpactEstimates?.totalSamples.annualizedCo2 || 0, + formatValue: asWeight, + }), + costImpactDiff: calculateBaseComparisonDiff({ + baselineValue: baseImpactEstimates?.totalSamples.annualizedDollarCost || 0, + comparisonValue: comparisonImpactEstimates?.totalSamples.annualizedDollarCost || 0, + formatValue: asCost, + }), + }; + }, [ + baselineDuration, + baselineScaledTotalSamples, + baselineTopNFunctions, + comparisonDuration, + comparisonScaledTotalSamples, + comparisonTopNFunctions, + ]); const data = [ { @@ -62,14 +98,16 @@ export function TopNFunctionsSummary({ defaultMessage: '{label} overall performance by', values: { label: - isLoading || totalSamplesDiff.percentDiffDelta === undefined + isLoading || + totalSamplesDiff.percentDiffDelta === undefined || + totalSamplesDiff.label === undefined ? 'Gained/Lost' : totalSamplesDiff?.percentDiffDelta > 0 ? 'Lost' : 'Gained', }, }) as string, - baseValue: totalSamplesDiff.label || '', + baseValue: totalSamplesDiff.label || '0%', baseIcon: totalSamplesDiff.icon, baseColor: totalSamplesDiff.color, titleHint: ESTIMATED_VALUE_LABEL, diff --git a/x-pack/plugins/profiling/public/components/topn_functions_summary/summary_item.test.ts b/x-pack/plugins/profiling/public/components/topn_functions_summary/summary_item.test.ts new file mode 100644 index 0000000000000..6b82d3d8c819f --- /dev/null +++ b/x-pack/plugins/profiling/public/components/topn_functions_summary/summary_item.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getValueLable } from './summary_item'; + +describe('Summary item', () => { + it('returns value and percentage', () => { + expect(getValueLable('10', '1%')).toEqual('10 (1%)'); + }); + + it('returns value', () => { + expect(getValueLable('10', undefined)).toEqual('10'); + }); + + it('returns value when perc is an empty string', () => { + expect(getValueLable('10', '')).toEqual('10'); + }); +}); diff --git a/x-pack/plugins/profiling/public/components/topn_functions_summary/summary_item.tsx b/x-pack/plugins/profiling/public/components/topn_functions_summary/summary_item.tsx index bf41625f7224d..a40ac315de7da 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions_summary/summary_item.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions_summary/summary_item.tsx @@ -56,6 +56,10 @@ function BaseValue({ value, icon, color }: { value: string; icon?: string; color ); } +export function getValueLable(value: string, perc?: string) { + return perc ? `${value} (${perc})` : `${value}`; +} + export function SummaryItem({ baseValue, baseIcon, @@ -98,7 +102,7 @@ export function SummaryItem({ {!isLoading && comparisonValue ? ( {comparisonIcon ? : null} - {`${comparisonValue} (${comparisonPerc})`} + {getValueLable(comparisonValue, comparisonPerc)} ) : null} diff --git a/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx b/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx index 107c11e2203ae..7c1ba0e144383 100644 --- a/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx +++ b/x-pack/plugins/profiling/public/views/functions/differential_topn/index.tsx @@ -206,6 +206,8 @@ export function DifferentialTopNFunctionsView() { state.status === AsyncStatus.Loading || comparisonState.status === AsyncStatus.Loading } + baselineDuration={totalSeconds} + comparisonDuration={totalComparisonSeconds} /> @@ -236,13 +238,11 @@ export function DifferentialTopNFunctionsView() { ; export const RuleFalsePositiveArray = t.array(t.string); // should be non-empty strings? +/** + * User defined fields to display in areas such as alert details and exceptions auto-populate + * Field added in PR - https://github.com/elastic/kibana/pull/163235 + * @example const investigationFields: RuleCustomHighlightedFieldArray = ['host.os.name'] + */ +export type RuleCustomHighlightedFieldArray = t.TypeOf; +export const RuleCustomHighlightedFieldArray = t.array(NonEmptyString); + export type RuleReferenceArray = t.TypeOf; export const RuleReferenceArray = t.array(t.string); // should be non-empty strings? diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts index a49112a98f81d..9d15c355df60d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts @@ -1289,6 +1289,36 @@ describe('rules schema', () => { expect(message.schema).toEqual({}); expect(getPaths(left(message.errors))).toEqual(['invalid keys "data_view_id"']); }); + + test('You can optionally send in an array of investigation_fields', () => { + const payload: RuleCreateProps = { + ...getCreateRulesSchemaMock(), + investigation_fields: ['field1', 'field2'], + }; + + const decoded = RuleCreateProps.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You cannot send in an array of investigation_fields that are numbers', () => { + const payload = { + ...getCreateRulesSchemaMock(), + investigation_fields: [0, 1, 2], + }; + + const decoded = RuleCreateProps.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "investigation_fields"', + 'Invalid value "1" supplied to "investigation_fields"', + 'Invalid value "2" supplied to "investigation_fields"', + ]); + expect(message.schema).toEqual({}); + }); }); describe('response', () => { diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts index 521e9918a6521..20d2c11042516 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts @@ -64,6 +64,7 @@ const getResponseBaseParams = (anchorDate: string = ANCHOR_DATE): SharedResponse timestamp_override: undefined, timestamp_override_fallback_disabled: undefined, namespace: undefined, + investigation_fields: undefined, }); export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryRule => ({ @@ -77,6 +78,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryRule saved_id: undefined, response_actions: undefined, alert_suppression: undefined, + investigation_fields: undefined, }); export const getSavedQuerySchemaMock = (anchorDate: string = ANCHOR_DATE): SavedQueryRule => ({ diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts index 0032ee60267c4..cd1562b1c4c48 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts @@ -232,4 +232,65 @@ describe('Rule response schema', () => { expect(message.schema).toEqual({}); }); }); + + describe('investigation_fields', () => { + test('it should validate rule with empty array for "investigation_fields"', () => { + const payload = getRulesSchemaMock(); + payload.investigation_fields = []; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = { ...getRulesSchemaMock(), investigation_fields: [] }; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should validate rule with "investigation_fields"', () => { + const payload = getRulesSchemaMock(); + payload.investigation_fields = ['foo', 'bar']; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = { ...getRulesSchemaMock(), investigation_fields: ['foo', 'bar'] }; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should validate undefined for "investigation_fields"', () => { + const payload: RuleResponse = { + ...getRulesSchemaMock(), + investigation_fields: undefined, + }; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = { ...getRulesSchemaMock(), investigation_fields: undefined }; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a string for "investigation_fields"', () => { + const payload: Omit & { + investigation_fields: string; + } = { + ...getRulesSchemaMock(), + investigation_fields: 'foo', + }; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "foo" supplied to "investigation_fields"', + ]); + expect(message.schema).toEqual({}); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts index 834607906b2e5..24badba560b8e 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts @@ -53,6 +53,7 @@ import { RelatedIntegrationArray, RequiredFieldArray, RuleAuthorArray, + RuleCustomHighlightedFieldArray, RuleDescription, RuleFalsePositiveArray, RuleFilterArray, @@ -116,6 +117,7 @@ export const baseSchema = buildRuleSchemas({ output_index: AlertsIndex, namespace: AlertsIndexNamespace, meta: RuleMetadata, + investigation_fields: RuleCustomHighlightedFieldArray, // Throttle throttle: RuleActionThrottle, }, diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 449092e86130a..8393ef508c097 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -112,7 +112,7 @@ export const allowedExperimentalValues = Object.freeze({ * Enables Discover embedded within timeline * * */ - discoverInTimeline: true, + discoverInTimeline: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index c6a022ff0998f..a92ec9901d7ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -141,6 +141,45 @@ describe('AlertSummaryView', () => { }); }); }); + test('User specified investigation fields appear in summary rows', async () => { + const mockData = mockAlertDetailsData.map((item) => { + if (item.category === 'event' && item.field === 'event.category') { + return { + ...item, + values: ['network'], + originalValue: ['network'], + }; + } + return item; + }); + const renderProps = { + ...props, + investigationFields: ['custom.field'], + data: [ + ...mockData, + { category: 'custom', field: 'custom.field', values: ['blob'], originalValue: 'blob' }, + ] as TimelineEventsDetailsItem[], + }; + await act(async () => { + const { getByText } = render( + + + + ); + + [ + 'custom.field', + 'host.name', + 'user.name', + 'destination.address', + 'source.address', + 'source.port', + 'process.name', + ].forEach((fieldId) => { + expect(getByText(fieldId)); + }); + }); + }); test('Network event renders the correct summary rows', async () => { const renderProps = { ...props, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index 5704eab502ed8..4eb81ddf5770f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -21,10 +21,30 @@ const AlertSummaryViewComponent: React.FC<{ title: string; goToTable: () => void; isReadOnly?: boolean; -}> = ({ browserFields, data, eventId, isDraggable, scopeId, title, goToTable, isReadOnly }) => { + investigationFields?: string[]; +}> = ({ + browserFields, + data, + eventId, + isDraggable, + scopeId, + title, + goToTable, + isReadOnly, + investigationFields, +}) => { const summaryRows = useMemo( - () => getSummaryRows({ browserFields, data, eventId, isDraggable, scopeId, isReadOnly }), - [browserFields, data, eventId, isDraggable, scopeId, isReadOnly] + () => + getSummaryRows({ + browserFields, + data, + eventId, + isDraggable, + scopeId, + isReadOnly, + investigationFields, + }), + [browserFields, data, eventId, isDraggable, scopeId, isReadOnly, investigationFields] ); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index ff5d44d2b9eb9..87f450ecb43b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -21,6 +21,8 @@ import styled from 'styled-components'; import { isEmpty } from 'lodash'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import type { RawEventData } from '../../../../common/types/response_actions'; import { useResponseActionsView } from './response_actions_view'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; @@ -169,6 +171,8 @@ const EventDetailsComponent: React.FC = ({ const goToTableTab = useCallback(() => setSelectedTabId(EventsViewType.tableView), []); const eventFields = useMemo(() => getEnrichmentFields(data), [data]); + const { ruleId } = useBasicDataFromDetailsData(data); + const { rule: maybeRule } = useRuleWithFallback(ruleId); const existingEnrichments = useMemo( () => isAlert @@ -284,6 +288,7 @@ const EventDetailsComponent: React.FC = ({ isReadOnly, }} goToTable={goToTableTab} + investigationFields={maybeRule?.investigation_fields ?? []} /> = ({ userRisk, allEnrichments, isEnrichmentsLoading, + maybeRule, ] ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx index 82be0711b75cc..160a91a9874ca 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -215,6 +215,15 @@ function getFieldsByRuleType(ruleType?: string): EventSummaryField[] { } } +/** + * Gets the fields to display based on custom rules and configuration + * @param customs The list of custom-defined fields to display + * @returns The list of custom-defined fields to display + */ +function getHighlightedFieldsOverride(customs: string[]): EventSummaryField[] { + return customs.map((field) => ({ id: field })); +} + /** This function is exported because it is used in the Exception Component to populate the conditions with the Highlighted Fields. Additionally, the new @@ -229,12 +238,15 @@ export function getEventFieldsToDisplay({ eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride, }: { eventCategories: EventCategories; eventCode?: string; eventRuleType?: string; + highlightedFieldsOverride: string[]; }): EventSummaryField[] { const fields = [ + ...getHighlightedFieldsOverride(highlightedFieldsOverride), ...alwaysDisplayedFields, ...getFieldsByCategory(eventCategories), ...getFieldsByEventCode(eventCode, eventCategories), @@ -281,11 +293,13 @@ export const getSummaryRows = ({ eventId, isDraggable = false, isReadOnly = false, + investigationFields, }: { data: TimelineEventsDetailsItem[]; browserFields: BrowserFields; scopeId: string; eventId: string; + investigationFields?: string[]; isDraggable?: boolean; isReadOnly?: boolean; }) => { @@ -306,6 +320,7 @@ export const getSummaryRows = ({ eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride: investigationFields ?? [], }); return data != null diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/use_timelines_events.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/use_timelines_events.tsx index 94d01bd7162ed..3a70ccebb9ca6 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/use_timelines_events.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/use_timelines_events.tsx @@ -261,8 +261,6 @@ export const useTimelineEventsHandler = ({ totalCount: response.totalCount, updatedAt: Date.now(), }; - setUpdated(newTimelineResponse.updatedAt); - setTotalCount(newTimelineResponse.totalCount); if (onNextHandler) onNextHandler(newTimelineResponse); return newTimelineResponse; }); @@ -294,19 +292,7 @@ export const useTimelineEventsHandler = ({ asyncSearch(); refetch.current = asyncSearch; }, - [ - skip, - data, - setTotalCount, - entityType, - dataViewId, - setUpdated, - addWarning, - startTracking, - dispatch, - id, - prevFilterStatus, - ] + [skip, data, entityType, dataViewId, addWarning, startTracking, dispatch, id, prevFilterStatus] ); useEffect(() => { @@ -392,6 +378,13 @@ export const useTimelineEventsHandler = ({ filterStatus, ]); + useEffect(() => { + if (timelineResponse.totalCount > -1) { + setUpdated(timelineResponse.updatedAt); + setTotalCount(timelineResponse.totalCount); + } + }, [setTotalCount, setUpdated, timelineResponse]); + const timelineEventsSearchHandler = useCallback( (onNextHandler?: OnNextResponseHandler) => { if (!deepEqual(prevTimelineRequest.current, timelineRequest)) { diff --git a/x-pack/plugins/security_solution/public/common/components/filter_group/index.scss b/x-pack/plugins/security_solution/public/common/components/filter_group/index.scss index 743873ec55674..6bdcb0e1cdd44 100644 --- a/x-pack/plugins/security_solution/public/common/components/filter_group/index.scss +++ b/x-pack/plugins/security_solution/public/common/components/filter_group/index.scss @@ -14,10 +14,15 @@ .euiFlexGroup.controlGroup { min-height: 34px; } + .euiFormControlLayout.euiFormControlLayout--group.controlFrame__formControlLayout { height: 34px; & .euiFormLabel.controlFrame__formControlLayoutLabel { - padding: 8px; + padding: 8px !important; + } + + .euiButtonEmpty.euiFilterButton { + height: 32px; } } .euiText.errorEmbeddableCompact__button { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index 1ace41854898b..64d289cd65f3e 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -18,7 +18,6 @@ import React, { } from 'react'; import { EuiMarkdownEditor } from '@elastic/eui'; import type { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; -import { useLicense } from '../../hooks/use_license'; import { uiPlugins, parsingPlugins, processingPlugins } from './plugins'; import { useUpsellingMessage } from '../../hooks/use_upselling'; @@ -72,12 +71,10 @@ const MarkdownEditorComponent = forwardRef { - return uiPlugins({ licenseIsPlatinum, insightsUpsellingMessage }); - }, [licenseIsPlatinum, insightsUpsellingMessage]); + return uiPlugins({ insightsUpsellingMessage }); + }, [insightsUpsellingMessage]); // @ts-expect-error update types useImperativeHandle(ref, () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts index 681caa3dd4cb8..ed2c60ea2e961 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/index.ts @@ -27,15 +27,12 @@ export const { export const platinumOnlyPluginTokens = [insightMarkdownPlugin.insightPrefix]; export const uiPlugins = ({ - licenseIsPlatinum, insightsUpsellingMessage, }: { - licenseIsPlatinum: boolean; insightsUpsellingMessage: string | null; }) => { const currentPlugins = nonStatefulUiPlugins.map((plugin) => plugin.name); const insightPluginWithLicense = insightMarkdownPlugin.plugin({ - licenseIsPlatinum, insightsUpsellingMessage, }); if (currentPlugins.includes(insightPluginWithLicense.name) === false) { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx index e29ad4ba89eb2..2b4ae4d2d9fcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.test.tsx @@ -134,35 +134,21 @@ describe('insight component renderer', () => { describe('plugin', () => { it('renders insightsUpsellingMessage when provided', () => { const insightsUpsellingMessage = 'test message'; - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage }); + const result = plugin({ insightsUpsellingMessage }); expect(result.button.label).toEqual(insightsUpsellingMessage); }); it('disables the button when insightsUpsellingMessage is provided', () => { const insightsUpsellingMessage = 'test message'; - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage }); + const result = plugin({ insightsUpsellingMessage }); expect(result.button.isDisabled).toBeTruthy(); }); - it('disables the button when license is not Platinum', () => { - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage: null }); - - expect(result.button.isDisabled).toBeTruthy(); - }); - - it('show investigate message when license is Platinum', () => { - const result = plugin({ licenseIsPlatinum: true, insightsUpsellingMessage: null }); + it('show investigate message when insightsUpsellingMessage is not provided', () => { + const result = plugin({ insightsUpsellingMessage: null }); expect(result.button.label).toEqual('Investigate'); }); - - it('show upsell message when license is not Platinum', () => { - const result = plugin({ licenseIsPlatinum: false, insightsUpsellingMessage: null }); - - expect(result.button.label).toEqual( - 'Upgrade to platinum to make use of insights in investigation guides' - ); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx index 81f1a77768189..7efbebb776cd7 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/index.tsx @@ -542,20 +542,16 @@ const exampleInsight = `${insightPrefix}{ }}`; export const plugin = ({ - licenseIsPlatinum, insightsUpsellingMessage, }: { - licenseIsPlatinum: boolean; insightsUpsellingMessage: string | null; }) => { - const label = licenseIsPlatinum ? i18n.INVESTIGATE : i18n.INSIGHT_UPSELL; - return { name: 'insights', button: { - label: insightsUpsellingMessage ?? label, + label: insightsUpsellingMessage ?? i18n.INVESTIGATE, iconType: 'timelineWithArrow', - isDisabled: !licenseIsPlatinum || !!insightsUpsellingMessage, + isDisabled: !!insightsUpsellingMessage, }, helpText: (
      diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts index e33a1f0d73539..1f2da4b0dcdd8 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/insight/translations.ts @@ -11,10 +11,6 @@ export const LABEL = i18n.translate('xpack.securitySolution.markdown.insight.lab defaultMessage: 'Label', }); -export const INSIGHT_UPSELL = i18n.translate('xpack.securitySolution.markdown.insight.upsell', { - defaultMessage: 'Upgrade to platinum to make use of insights in investigation guides', -}); - export const INVESTIGATE = i18n.translate('xpack.securitySolution.markdown.insight.title', { defaultMessage: 'Investigate', }); diff --git a/x-pack/plugins/security_solution/public/common/components/paywall/index.tsx b/x-pack/plugins/security_solution/public/common/components/paywall/index.tsx deleted file mode 100644 index ee93861db2d7e..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/paywall/index.tsx +++ /dev/null @@ -1,94 +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, useCallback } from 'react'; -import { - EuiCard, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiButton, - EuiTextColor, - EuiImage, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { useNavigation } from '../../lib/kibana'; -import * as i18n from './translations'; -import paywallPng from '../../images/entity_paywall.png'; - -const PaywallDiv = styled.div` - max-width: 75%; - margin: 0 auto; - .euiCard__betaBadgeWrapper { - .euiCard__betaBadge { - width: auto; - } - } - .platinumCardDescription { - padding: 0 15%; - } -`; -const StyledEuiCard = styled(EuiCard)` - span.euiTitle { - max-width: 540px; - display: block; - margin: 0 auto; - } -`; - -export const Paywall = memo(({ heading }: { heading?: string }) => { - const { getAppUrl, navigateTo } = useNavigation(); - const subscriptionUrl = getAppUrl({ - appId: 'management', - path: 'stack/license_management', - }); - const goToSubscription = useCallback(() => { - navigateTo({ url: subscriptionUrl }); - }, [navigateTo, subscriptionUrl]); - return ( - - } - display="subdued" - title={ -

      - {heading} -

      - } - description={false} - paddingSize="xl" - > - - - -

      - {i18n.UPGRADE_MESSAGE} -

      -
      - -
      - - {i18n.UPGRADE_BUTTON} - -
      -
      -
      -
      -
      - - - - - -
      - ); -}); - -Paywall.displayName = 'Paywall'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx index 6469b0bcffb17..cd70445dcebae 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upselling.test.tsx @@ -35,7 +35,7 @@ const RenderWrapper: React.FunctionComponent = ({ children }) => { describe('use_upselling', () => { test('useUpsellingComponent returns sections', () => { - mockUpselling.registerSections({ + mockUpselling.setSections({ entity_analytics_panel: TestComponent, }); @@ -47,7 +47,7 @@ describe('use_upselling', () => { }); test('useUpsellingPage returns pages', () => { - mockUpselling.registerPages({ + mockUpselling.setPages({ [SecurityPageName.hosts]: TestComponent, }); @@ -57,9 +57,9 @@ describe('use_upselling', () => { expect(result.current).toBe(TestComponent); }); - test('useUpsellingMessage returns pages', () => { + test('useUpsellingMessage returns messages', () => { const testMessage = 'test message'; - mockUpselling.registerMessages({ + mockUpselling.setMessages({ investigation_guide: testMessage, }); @@ -72,7 +72,7 @@ describe('use_upselling', () => { test('useUpsellingMessage returns null when upsellingMessageId not found', () => { const emptyMessages = {}; - mockUpselling.registerMessages(emptyMessages); + mockUpselling.setPages(emptyMessages); const { result } = renderHook( () => useUpsellingMessage('my_fake_message_id' as 'investigation_guide'), diff --git a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx index 4011df08ad0cb..2083e6c21f5c2 100644 --- a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.test.tsx @@ -14,7 +14,7 @@ const TestComponent = () =>
      {'TEST component'}
      ; describe('UpsellingService', () => { it('registers sections', async () => { const service = new UpsellingService(); - service.registerSections({ + service.setSections({ entity_analytics_panel: TestComponent, }); @@ -23,9 +23,24 @@ describe('UpsellingService', () => { expect(value.get('entity_analytics_panel')).toEqual(TestComponent); }); + it('overwrites registered sections when called twice', async () => { + const service = new UpsellingService(); + service.setSections({ + entity_analytics_panel: TestComponent, + }); + + service.setSections({ + osquery_automated_response_actions: TestComponent, + }); + + const value = await firstValueFrom(service.sections$); + + expect(Array.from(value.keys())).toEqual(['osquery_automated_response_actions']); + }); + it('registers pages', async () => { const service = new UpsellingService(); - service.registerPages({ + service.setPages({ [SecurityPageName.hosts]: TestComponent, }); @@ -34,10 +49,25 @@ describe('UpsellingService', () => { expect(value.get(SecurityPageName.hosts)).toEqual(TestComponent); }); + it('overwrites registered pages when called twice', async () => { + const service = new UpsellingService(); + service.setPages({ + [SecurityPageName.hosts]: TestComponent, + }); + + service.setPages({ + [SecurityPageName.users]: TestComponent, + }); + + const value = await firstValueFrom(service.pages$); + + expect(Array.from(value.keys())).toEqual([SecurityPageName.users]); + }); + it('registers messages', async () => { const testMessage = 'test message'; const service = new UpsellingService(); - service.registerMessages({ + service.setMessages({ investigation_guide: testMessage, }); @@ -46,9 +76,23 @@ describe('UpsellingService', () => { expect(value.get('investigation_guide')).toEqual(testMessage); }); + it('overwrites registered messages when called twice', async () => { + const testMessage = 'test message'; + const service = new UpsellingService(); + service.setMessages({ + investigation_guide: testMessage, + }); + + service.setMessages({}); + + const value = await firstValueFrom(service.messages$); + + expect(Array.from(value.keys())).toEqual([]); + }); + it('"isPageUpsellable" returns true when page is upsellable', () => { const service = new UpsellingService(); - service.registerPages({ + service.setPages({ [SecurityPageName.hosts]: TestComponent, }); @@ -57,7 +101,7 @@ describe('UpsellingService', () => { it('"getPageUpselling" returns page component when page is upsellable', () => { const service = new UpsellingService(); - service.registerPages({ + service.setPages({ [SecurityPageName.hosts]: TestComponent, }); diff --git a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts index 14d4949e8ea4b..27d15c1d12768 100644 --- a/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts +++ b/x-pack/plugins/security_solution/public/common/lib/upsellings/upselling_service.ts @@ -43,24 +43,33 @@ export class UpsellingService { this.messages$ = this.messagesSubject$.asObservable(); } - registerSections(sections: SectionUpsellings) { + setSections(sections: SectionUpsellings) { + this.sections.clear(); + Object.entries(sections).forEach(([sectionId, component]) => { this.sections.set(sectionId as UpsellingSectionId, component); }); + this.sectionsSubject$.next(this.sections); } - registerPages(pages: PageUpsellings) { + setPages(pages: PageUpsellings) { + this.pages.clear(); + Object.entries(pages).forEach(([pageId, component]) => { this.pages.set(pageId as SecurityPageName, component); }); + this.pagesSubject$.next(this.pages); } - registerMessages(messages: MessageUpsellings) { + setMessages(messages: MessageUpsellings) { + this.messages.clear(); + Object.entries(messages).forEach(([messageId, component]) => { this.messages.set(messageId as UpsellingMessageId, component); }); + this.messagesSubject$.next(this.messages); } diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.tsx b/x-pack/plugins/security_solution/public/common/links/links.test.tsx index b1ebd10d07851..7e00e39f75437 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.tsx +++ b/x-pack/plugins/security_solution/public/common/links/links.test.tsx @@ -182,9 +182,9 @@ describe('Security links', () => { expect(result.current).toStrictEqual([networkLinkItem]); }); - it('should return unauthorized page when page has upselling', async () => { + it('should return unauthorized page when page has upselling (serverless)', async () => { const upselling = new UpsellingService(); - upselling.registerPages({ [SecurityPageName.network]: () => }); + upselling.setPages({ [SecurityPageName.network]: () => }); const { result, waitForNextUpdate } = renderUseAppLinks(); const networkLinkItem = { @@ -192,8 +192,6 @@ describe('Security links', () => { title: 'Network', path: '/network', capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${CASES_FEATURE_ID}.write_cases`], - experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, - hideWhenExperimentalKey: 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, licenseType: 'basic' as const, }; @@ -249,6 +247,67 @@ describe('Security links', () => { expect(result.current).toStrictEqual([{ ...networkLinkItem, unauthorized: true }]); }); + + it('should return unauthorized page when page has upselling (ESS)', async () => { + const upselling = new UpsellingService(); + upselling.setPages({ [SecurityPageName.network]: () => }); + const { result, waitForNextUpdate } = renderUseAppLinks(); + const hostLinkItem = { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + licenseType: 'platinum' as const, + }; + + mockUpselling.setPages({ + [SecurityPageName.hosts]: () => , + }); + + await act(async () => { + updateAppLinks([hostLinkItem], { + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: { hasAtLeast: licenseBasicMock } as unknown as ILicense, + upselling: mockUpselling, + }); + await waitForNextUpdate(); + }); + expect(result.current).toStrictEqual([{ ...hostLinkItem, unauthorized: true }]); + + // cleanup + mockUpselling.setPages({}); + }); + + it('should filter out experimental page even if it has upselling', async () => { + const upselling = new UpsellingService(); + upselling.setPages({ [SecurityPageName.network]: () => }); + const { result, waitForNextUpdate } = renderUseAppLinks(); + const hostLinkItem = { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + licenseType: 'platinum' as const, + experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, + }; + + mockUpselling.setPages({ + [SecurityPageName.hosts]: () => , + }); + + await act(async () => { + updateAppLinks([hostLinkItem], { + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: { hasAtLeast: licenseBasicMock } as unknown as ILicense, + upselling: mockUpselling, + }); + await waitForNextUpdate(); + }); + expect(result.current).toStrictEqual([]); + + // cleanup + mockUpselling.setPages({}); + }); }); describe('useLinkExists', () => { diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index 016a6a6e4fa19..e519ace88336a 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -153,10 +153,14 @@ const getNormalizedLink = (id: SecurityPageName): Readonly | und const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissions): LinkItem[] => appLinks.reduce((acc, { links, ...appLinkWithoutSublinks }) => { - if (!isLinkAllowed(appLinkWithoutSublinks, linksPermissions)) { + if (!isLinkExperimentalKeyAllowed(appLinkWithoutSublinks, linksPermissions)) { return acc; } - if (!hasCapabilities(linksPermissions.capabilities, appLinkWithoutSublinks.capabilities)) { + + if ( + !hasCapabilities(linksPermissions.capabilities, appLinkWithoutSublinks.capabilities) || + !isLinkLicenseAllowed(appLinkWithoutSublinks, linksPermissions) + ) { if (linksPermissions.upselling.isPageUpsellable(appLinkWithoutSublinks.id)) { acc.push({ ...appLinkWithoutSublinks, unauthorized: true }); } @@ -175,7 +179,21 @@ const processAppLinks = (appLinks: AppLinkItems, linksPermissions: LinksPermissi return acc; }, []); -const isLinkAllowed = (link: LinkItem, { license, experimentalFeatures }: LinksPermissions) => { +const isLinkExperimentalKeyAllowed = ( + link: LinkItem, + { experimentalFeatures }: LinksPermissions +) => { + if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) { + return false; + } + + if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) { + return false; + } + return true; +}; + +const isLinkLicenseAllowed = (link: LinkItem, { license }: LinksPermissions) => { const linkLicenseType = link.licenseType ?? 'basic'; if (license) { if (!license.hasAtLeast(linkLicenseType)) { @@ -184,11 +202,5 @@ const isLinkAllowed = (link: LinkItem, { license, experimentalFeatures }: LinksP } else if (linkLicenseType !== 'basic') { return false; } - if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) { - return false; - } - if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) { - return false; - } return true; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index 80a7971aedfc1..5f1a36f931026 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -555,6 +555,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -635,6 +636,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -659,6 +661,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -702,6 +705,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -754,6 +758,7 @@ describe('helpers', () => { ], }, ], + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -782,6 +787,95 @@ describe('helpers', () => { threat: getThreatMock(), timestamp_override: 'event.ingest', timestamp_override_fallback_disabled: true, + investigation_fields: ['foo', 'bar'], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if investigation_fields is empty array', () => { + const mockStepData: AboutStepRule = { + ...mockData, + investigationFields: [], + }; + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { + author: ['Elastic'], + description: '24/7', + false_positives: ['test'], + license: 'Elastic License', + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + tags: ['tag1', 'tag2'], + rule_name_override: undefined, + threat_indicator_path: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + threat: getThreatMock(), + investigation_fields: [], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with investigation_fields', () => { + const mockStepData: AboutStepRule = { + ...mockData, + investigationFields: ['foo', 'bar'], + }; + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { + author: ['Elastic'], + description: '24/7', + false_positives: ['test'], + license: 'Elastic License', + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + tags: ['tag1', 'tag2'], + threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], + threat_indicator_path: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if investigation_fields includes empty string', () => { + const mockStepData: AboutStepRule = { + ...mockData, + investigationFields: [' '], + }; + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { + author: ['Elastic'], + description: '24/7', + false_positives: ['test'], + license: 'Elastic License', + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + tags: ['tag1', 'tag2'], + threat: getThreatMock(), + investigation_fields: [], + threat_indicator_path: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index ea466771bf8ae..c3cb11e907a14 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -485,6 +485,7 @@ export const formatAboutStepData = ( const { author, falsePositives, + investigationFields, references, riskScore, severity, @@ -524,6 +525,7 @@ export const formatAboutStepData = ( : {}), false_positives: falsePositives.filter((item) => !isEmpty(item)), references: references.filter((item) => !isEmpty(item)), + investigation_fields: investigationFields.filter((item) => !isEmpty(item.trim())), risk_score: riskScore.value, risk_score_mapping: riskScore.isMappingChecked ? riskScore.mapping.filter((m) => m.field != null && m.field !== '') diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx index 2e173812b0109..6150cb3b6d0e8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx @@ -662,6 +662,42 @@ describe('When the add exception modal is opened', () => { expect(getByTestId('entryType')).toHaveTextContent('match'); expect(getByTestId('entryValue')).toHaveTextContent('test/path'); }); + + it('should include rule defined custom highlighted fields', () => { + const wrapper = render( + (() => ( + + + + ))() + ); + const { getByTestId, getAllByTestId } = wrapper; + expect(getByTestId('alertExceptionBuilder')).toBeInTheDocument(); + expect(getAllByTestId('entryField')[0]).toHaveTextContent('foo.bar'); + expect(getAllByTestId('entryOperator')[0]).toHaveTextContent('included'); + expect(getAllByTestId('entryType')[0]).toHaveTextContent('match'); + expect(getAllByTestId('entryValue')[0]).toHaveTextContent('blob'); + expect(getAllByTestId('entryField')[1]).toHaveTextContent('file.path'); + expect(getAllByTestId('entryOperator')[1]).toHaveTextContent('included'); + expect(getAllByTestId('entryType')[1]).toHaveTextContent('match'); + expect(getAllByTestId('entryValue')[1]).toHaveTextContent('test/path'); + }); }); describe('bulk closeable alert data is passed in', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx index 41bcefebeb191..d7081f195fefc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx @@ -346,6 +346,9 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ const populatedException = getPrepopulatedRuleExceptionWithHighlightFields({ alertData, exceptionItemName, + // With "rule_default" type, there is only ever one rule associated. + // That is why it's ok to pull just the first item from rules array here. + ruleCustomHighlightedFields: rules?.[0]?.investigation_fields ?? [], }); if (populatedException) { setComment(i18n.ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT(alertData._id)); @@ -354,7 +357,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ } } } - }, [listType, exceptionItemName, alertData, setInitialExceptionItems, setComment]); + }, [listType, exceptionItemName, alertData, rules, setInitialExceptionItems, setComment]); const osTypesSelection = useMemo((): OsTypeArray => { return hasAlertData ? retrieveAlertOsTypes(alertData) : selectedOs ? [...selectedOs] : []; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx index 33b1a53ad1b15..e7a3d40dd9ad7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx @@ -1560,6 +1560,27 @@ describe('Exception helpers', () => { }, { field: 'process.name', operator: 'included', type: 'match', value: 'malware writer' }, ]; + const expectedExceptionEntriesWithCustomHighlightedFields = [ + { + field: 'event.type', + operator: 'included', + type: 'match', + value: 'creation', + }, + { + field: 'agent.id', + operator: 'included', + type: 'match', + value: 'f4f86e7c-29bd-4655-b7d0-a3d08ad0c322', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'C:/malware.exe', + }, + { field: 'process.name', operator: 'included', type: 'match', value: 'malware writer' }, + ]; const entriesWithMatchAny = { field: 'Endpoint.capabilities', operator, @@ -1739,12 +1760,12 @@ describe('Exception helpers', () => { }, ]; it('should return the highlighted fields correctly when eventCode, eventCategory and RuleType are in the alertData', () => { - const res = getAlertHighlightedFields(alertData); + const res = getAlertHighlightedFields(alertData, []); expect(res).toEqual(allHighlightFields); }); it('should return highlighted fields without the file.Ext.quarantine_path when "event.code" is not in the alertData', () => { const alertDataWithoutEventCode = { ...alertData, 'event.code': null }; - const res = getAlertHighlightedFields(alertDataWithoutEventCode); + const res = getAlertHighlightedFields(alertDataWithoutEventCode, []); expect(res).toEqual([ ...baseGeneratedAlertHighlightedFields, { @@ -1763,7 +1784,7 @@ describe('Exception helpers', () => { }); it('should return highlighted fields without the file and process props when "event.category" is not in the alertData', () => { const alertDataWithoutEventCategory = { ...alertData, 'event.category': null }; - const res = getAlertHighlightedFields(alertDataWithoutEventCategory); + const res = getAlertHighlightedFields(alertDataWithoutEventCategory, []); expect(res).toEqual([ ...baseGeneratedAlertHighlightedFields, { @@ -1775,7 +1796,7 @@ describe('Exception helpers', () => { }); it('should return the process highlighted fields correctly when eventCategory is an array', () => { const alertDataEventCategoryProcessArray = { ...alertData, 'event.category': ['process'] }; - const res = getAlertHighlightedFields(alertDataEventCategoryProcessArray); + const res = getAlertHighlightedFields(alertDataEventCategoryProcessArray, []); expect(res).not.toEqual( expect.arrayContaining([ { id: 'file.name' }, @@ -1793,20 +1814,20 @@ describe('Exception helpers', () => { }); it('should return all highlighted fields even when the "kibana.alert.rule.type" is not in the alertData', () => { const alertDataWithoutEventCategory = { ...alertData, 'kibana.alert.rule.type': null }; - const res = getAlertHighlightedFields(alertDataWithoutEventCategory); + const res = getAlertHighlightedFields(alertDataWithoutEventCategory, []); expect(res).toEqual(allHighlightFields); }); it('should return all highlighted fields when there are no fields to be filtered out', () => { jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] })); - const res = getAlertHighlightedFields(alertData); + const res = getAlertHighlightedFields(alertData, []); expect(res).toEqual(allHighlightFields); }); it('should exclude the "agent.id" from highlighted fields when agent.type is not "endpoint"', () => { jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] })); const alertDataWithoutAgentType = { ...alertData, agent: { ...alertData.agent, type: '' } }; - const res = getAlertHighlightedFields(alertDataWithoutAgentType); + const res = getAlertHighlightedFields(alertDataWithoutAgentType, []); expect(res).toEqual(allHighlightFields.filter((field) => field.id !== AGENT_ID)); }); @@ -1814,10 +1835,14 @@ describe('Exception helpers', () => { jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] })); const alertDataWithoutRuleUUID = { ...alertData, 'kibana.alert.rule.uuid': '' }; - const res = getAlertHighlightedFields(alertDataWithoutRuleUUID); + const res = getAlertHighlightedFields(alertDataWithoutRuleUUID, []); expect(res).toEqual(allHighlightFields.filter((field) => field.id !== AGENT_ID)); }); + it('should include custom highlighted fields', () => { + const res = getAlertHighlightedFields(alertData, ['event.type']); + expect(res).toEqual([{ id: 'event.type' }, ...allHighlightFields]); + }); }); describe('getPrepopulatedRuleExceptionWithHighlightFields', () => { it('should not create any exception and return null if there are no highlighted fields', () => { @@ -1826,6 +1851,7 @@ describe('Exception helpers', () => { const res = getPrepopulatedRuleExceptionWithHighlightFields({ alertData: defaultAlertData, exceptionItemName: '', + ruleCustomHighlightedFields: [], }); expect(res).toBe(null); }); @@ -1835,6 +1861,7 @@ describe('Exception helpers', () => { const res = getPrepopulatedRuleExceptionWithHighlightFields({ alertData: defaultAlertData, exceptionItemName: '', + ruleCustomHighlightedFields: [], }); expect(res).toBe(null); }); @@ -1842,6 +1869,7 @@ describe('Exception helpers', () => { const exception = getPrepopulatedRuleExceptionWithHighlightFields({ alertData, exceptionItemName: name, + ruleCustomHighlightedFields: [], }); expect(exception?.entries).toEqual( @@ -1849,6 +1877,21 @@ describe('Exception helpers', () => { ); expect(exception?.name).toEqual(name); }); + it('should create a new exception and populate its entries with the custom highlighted fields', () => { + const exception = getPrepopulatedRuleExceptionWithHighlightFields({ + alertData, + exceptionItemName: name, + ruleCustomHighlightedFields: ['event.type'], + }); + + expect(exception?.entries).toEqual( + expectedExceptionEntriesWithCustomHighlightedFields.map((entry) => ({ + ...entry, + id: '123', + })) + ); + expect(exception?.name).toEqual(name); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx index 3235276e650a2..617dd2901363c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx @@ -908,11 +908,13 @@ export const buildExceptionEntriesFromAlertFields = ({ export const getPrepopulatedRuleExceptionWithHighlightFields = ({ alertData, exceptionItemName, + ruleCustomHighlightedFields, }: { alertData: AlertData; exceptionItemName: string; + ruleCustomHighlightedFields: string[]; }): ExceptionsBuilderExceptionItem | null => { - const highlightedFields = getAlertHighlightedFields(alertData); + const highlightedFields = getAlertHighlightedFields(alertData, ruleCustomHighlightedFields); if (!highlightedFields.length) return null; const exceptionEntries = buildExceptionEntriesFromAlertFields({ highlightedFields, alertData }); @@ -951,11 +953,13 @@ export const filterHighlightedFields = ( * * Alert field ids filters * @param alertData The Alert data object */ -export const getAlertHighlightedFields = (alertData: AlertData): EventSummaryField[] => { +export const getAlertHighlightedFields = ( + alertData: AlertData, + ruleCustomHighlightedFields: string[] +): EventSummaryField[] => { const eventCategory = get(alertData, EVENT_CATEGORY); const eventCode = get(alertData, EVENT_CODE); const eventRuleType = get(alertData, KIBANA_ALERT_RULE_TYPE); - const eventCategories = { primaryEventCategory: Array.isArray(eventCategory) ? eventCategory[0] : eventCategory, allEventCategories: [eventCategory], @@ -965,6 +969,7 @@ export const getAlertHighlightedFields = (alertData: AlertData): EventSummaryFie eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride: ruleCustomHighlightedFields, }); return filterHighlightedFields(fieldsToDisplay, highlightedFieldsPrefixToExclude, alertData); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index 35441d926402b..63a6b70356ea3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -73,6 +73,7 @@ import { TimestampField, TimestampOverride, TimestampOverrideFallbackDisabled, + RuleCustomHighlightedFieldArray, } from '../../../../common/api/detection_engine/model/rule_schema'; import type { @@ -201,6 +202,7 @@ export const RuleSchema = t.intersection([ version: RuleVersion, execution_summary: RuleExecutionSummary, alert_suppression: AlertSuppression, + investigation_fields: RuleCustomHighlightedFieldArray, }), ]); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 487052fcbf2ef..d525a894a3af4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -194,6 +194,7 @@ export const mockAboutStepRule = (): AboutStepRule => ({ tags: ['tag1', 'tag2'], threat: getThreatMock(), note: '# this is some markdown documentation', + investigationFields: ['foo', 'bar'], }); export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index c2d477d5a87c6..0e1b32c3c77d9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -15,6 +15,7 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { TableId } from '@kbn/securitysolution-data-table'; +import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../common/components/header_actions'; import { isActiveTimeline } from '../../../../helpers'; import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item'; @@ -384,6 +385,7 @@ export const AddExceptionFlyoutWrapper: React.FC alertStatus, }) => { const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + const { rule: maybeRule, loading: isRuleLoading } = useRuleWithFallback(ruleId); const { loading: isLoadingAlertData, data } = useQueryAlerts({ query: buildGetAlertByIdQuery(eventId), @@ -429,32 +431,13 @@ export const AddExceptionFlyoutWrapper: React.FC return ruleDataViewId; }, [enrichedAlert, ruleDataViewId]); - // TODO: Do we want to notify user when they are working off of an older version of a rule - // if they select to add an exception from an alert referencing an older rule version? const memoRule = useMemo(() => { - if (enrichedAlert != null && enrichedAlert['kibana.alert.rule.parameters'] != null) { - return [ - { - ...enrichedAlert['kibana.alert.rule.parameters'], - id: ruleId, - rule_id: ruleRuleId, - name: ruleName, - index: memoRuleIndices, - data_view_id: memoDataViewId, - }, - ] as Rule[]; + if (maybeRule) { + return [maybeRule]; } - return [ - { - id: ruleId, - rule_id: ruleRuleId, - name: ruleName, - index: memoRuleIndices, - data_view_id: memoDataViewId, - }, - ] as Rule[]; - }, [enrichedAlert, memoDataViewId, memoRuleIndices, ruleId, ruleName, ruleRuleId]); + return null; + }, [maybeRule]); const isLoading = (isLoadingAlertData && isSignalIndexLoading) || @@ -466,7 +449,7 @@ export const AddExceptionFlyoutWrapper: React.FC rules={memoRule} isEndpointItem={exceptionListType === ExceptionListTypeEnum.ENDPOINT} alertData={enrichedAlert} - isAlertDataLoading={isLoading} + isAlertDataLoading={isLoading || isRuleLoading} alertStatus={alertStatus} isBulkAction={false} showAlertCloseOptions diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 8ea4e21509f5d..ecfcb5c981235 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -27,6 +27,7 @@ import { buildUrlsDescription, buildNoteDescription, buildRuleTypeDescription, + buildHighlightedFieldsOverrideDescription, } from './helpers'; import type { ListItems } from './types'; @@ -508,4 +509,28 @@ describe('helpers', () => { expect(result.description).toEqual('Indicator Match'); }); }); + + describe('buildHighlightedFieldsOverrideDescription', () => { + test('returns ListItem with passed in label and custom highlighted fields', () => { + const result: ListItems[] = buildHighlightedFieldsOverrideDescription('Test label', [ + 'foo', + 'bar', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + const element = wrapper.find( + '[data-test-subj="customHighlightedFieldsStringArrayDescriptionBadgeItem"]' + ); + + expect(result[0].title).toEqual('Test label'); + expect(element.exists()).toBeTruthy(); + expect(element.at(0).text()).toEqual('foo'); + expect(element.at(1).text()).toEqual('bar'); + }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildHighlightedFieldsOverrideDescription('Test label', []); + + expect(result).toHaveLength(0); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index d3d79b76b1d02..57fe4f72fd19f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -27,15 +27,13 @@ import { FieldIcon } from '@kbn/react-field'; import type { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public'; +import type { RequiredFieldArray } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes'; import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; -import type { - RequiredFieldArray, - Threshold, -} from '../../../../../common/api/detection_engine/model/rule_schema'; +import type { Threshold } from '../../../../../common/api/detection_engine/model/rule_schema'; import * as i18n from './translations'; import type { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; @@ -201,6 +199,38 @@ export const buildUnorderedListArrayDescription = ( return []; }; +export const buildHighlightedFieldsOverrideDescription = ( + label: string, + values: string[] +): ListItems[] => { + if (isEmpty(values)) { + return []; + } + const description = ( + + {values.map((val: string) => + isEmpty(val) ? null : ( + + + {val} + + + ) + )} + + ); + + return [ + { + title: label, + description, + }, + ]; +}; + export const buildStringArrayDescription = ( label: string, field: string, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index a2206c9c562e7..5ee4bb4bb1f47 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -262,7 +262,7 @@ describe('description_step', () => { mockLicenseService ); - expect(result.length).toEqual(11); + expect(result.length).toEqual(12); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index d4f7c026d369e..8d7c21e386e40 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -14,11 +14,11 @@ import type { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-a import type { DataViewBase, Filter } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; -import { buildRelatedIntegrationsDescription } from '../related_integrations/integrations_description'; import type { RelatedIntegrationArray, RequiredFieldArray, -} from '../../../../../common/api/detection_engine/model/rule_schema'; +} from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes'; +import { buildRelatedIntegrationsDescription } from '../related_integrations/integrations_description'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import { useKibana } from '../../../../common/lib/kibana'; @@ -47,6 +47,7 @@ import { buildAlertSuppressionDescription, buildAlertSuppressionWindowDescription, buildAlertSuppressionMissingFieldsDescription, + buildHighlightedFieldsOverrideDescription, } from './helpers'; import * as i18n from './translations'; import { buildMlJobsDescription } from './build_ml_jobs_description'; @@ -261,6 +262,9 @@ export const getDescriptionItem = ( } else if (field === 'falsePositives') { const values: string[] = get(field, data); return buildUnorderedListArrayDescription(label, field, values); + } else if (field === 'investigationFields') { + const values: string[] = get(field, data); + return buildHighlightedFieldsOverrideDescription(label, values); } else if (field === 'riskScore') { const values: AboutStepRiskScore = get(field, data); return buildRiskScoreDescription(values); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/index.tsx similarity index 62% rename from x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/index.tsx index 579167c73f4cb..9dbd49a2f4f07 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/index.tsx @@ -11,40 +11,44 @@ import { EuiToolTip } from '@elastic/eui'; import type { DataViewFieldBase } from '@kbn/es-query'; import type { FieldHook } from '../../../../shared_imports'; import { Field } from '../../../../shared_imports'; -import { GROUP_BY_FIELD_PLACEHOLDER, GROUP_BY_FIELD_LICENSE_WARNING } from './translations'; +import { FIELD_PLACEHOLDER } from './translations'; -interface GroupByFieldsProps { +interface MultiSelectAutocompleteProps { browserFields: DataViewFieldBase[]; isDisabled: boolean; field: FieldHook; + fullWidth?: boolean; + disabledText?: string; } const FIELD_COMBO_BOX_WIDTH = 410; -const fieldDescribedByIds = 'detectionEngineStepDefineRuleGroupByField'; +const fieldDescribedByIds = 'detectionEngineMultiSelectAutocompleteField'; -export const GroupByComponent: React.FC = ({ +export const MultiSelectAutocompleteComponent: React.FC = ({ browserFields, + disabledText, isDisabled, field, -}: GroupByFieldsProps) => { + fullWidth = false, +}: MultiSelectAutocompleteProps) => { const fieldEuiFieldProps = useMemo( () => ({ fullWidth: true, noSuggestions: false, options: browserFields.map((browserField) => ({ label: browserField.name })), - placeholder: GROUP_BY_FIELD_PLACEHOLDER, + placeholder: FIELD_PLACEHOLDER, onCreateOption: undefined, - style: { width: `${FIELD_COMBO_BOX_WIDTH}px` }, + ...(fullWidth ? {} : { style: { width: `${FIELD_COMBO_BOX_WIDTH}px` } }), isDisabled, }), - [browserFields, isDisabled] + [browserFields, isDisabled, fullWidth] ); const fieldComponent = ( ); return isDisabled ? ( - + {fieldComponent} ) : ( @@ -52,4 +56,4 @@ export const GroupByComponent: React.FC = ({ ); }; -export const GroupByFields = React.memo(GroupByComponent); +export const MultiSelectFieldsAutocomplete = React.memo(MultiSelectAutocompleteComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/translations.ts similarity index 54% rename from x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/translations.ts index d0df6a7320015..4dd83c607ef05 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/translations.ts @@ -7,16 +7,9 @@ import { i18n } from '@kbn/i18n'; -export const GROUP_BY_FIELD_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText', +export const FIELD_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.multiSelectFields.placeholderText', { defaultMessage: 'Select a field', } ); - -export const GROUP_BY_FIELD_LICENSE_WARNING = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning', - { - defaultMessage: 'Alert suppression is enabled with Platinum license or above', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts index 3ae5441d060d0..f8025537f3f17 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts @@ -26,6 +26,7 @@ export const stepAboutDefaultValue: AboutStepRule = { riskScore: { value: 21, mapping: [], isMappingChecked: false }, references: [''], falsePositives: [''], + investigationFields: [], license: '', ruleNameOverride: '', tags: [], diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 096de7b836a0b..a9805bf71d3ce 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -274,6 +274,7 @@ describe('StepAboutRuleComponent', () => { technique: [], }, ], + investigationFields: [], }; await act(async () => { @@ -333,6 +334,7 @@ describe('StepAboutRuleComponent', () => { technique: [], }, ], + investigationFields: [], }; await act(async () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 7052c29ce7881..622153160c9f4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -33,6 +33,7 @@ import { useFetchIndex } from '../../../../common/containers/source'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { useKibana } from '../../../../common/lib/kibana'; import { useRuleIndices } from '../../../../detection_engine/rule_management/logic/use_rule_indices'; +import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; const CommonUseField = getUseField({ component: Field }); @@ -237,6 +238,16 @@ const StepAboutRuleComponent: FC = ({ }} /> + + = { ), labelAppend: OptionalFieldLabel, }, + investigationFields: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldCustomHighlightedFieldsLabel', + { + defaultMessage: 'Custom highlighted fields', + } + ), + labelAppend: OptionalFieldLabel, + }, license: { type: FIELD_TYPES.TEXT, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index f1841430f03ff..007cf4d9dd4c6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -28,6 +28,13 @@ export const ADD_FALSE_POSITIVE = i18n.translate( } ); +export const ADD_CUSTOM_HIGHLIGHTED_FIELD = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addCustomHighlightedFieldDescription', + { + defaultMessage: 'Add a custom highlighted field', + } +); + export const GLOBAL_ENDPOINT_EXCEPTION_LIST = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 779fe5f35a820..a0da91e660566 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -75,7 +75,7 @@ import { NewTermsFields } from '../new_terms_fields'; import { ScheduleItem } from '../schedule_item_form'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; import { defaultCustomQuery } from '../../../pages/detection_engine/rules/utils'; -import { GroupByFields } from '../group_by_fields'; +import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; import { useLicense } from '../../../../common/hooks/use_license'; import { minimumLicenseForSuppression, @@ -752,9 +752,10 @@ const StepDefineRuleComponent: FC = ({ > { threat: getThreatMock(), timestampOverride: 'event.ingested', timestampOverrideFallbackDisabled: false, + investigationFields: [], }; const scheduleRuleStepData = { from: '0s', interval: '5m' }; const ruleActionsStepData = { @@ -181,6 +182,14 @@ describe('rule helpers', () => { expect(result.note).toEqual(''); }); + + test('returns customHighlightedField as empty array if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.investigation_fields; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); + + expect(result.investigationFields).toEqual([]); + }); }); describe('determineDetailsValue', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index e80ec9591c30c..7ef797d64eeb0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -200,6 +200,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu severity, false_positives: falsePositives, risk_score: riskScore, + investigation_fields: investigationFields, tags, threat, threat_indicator_path: threatIndicatorPath, @@ -230,6 +231,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu isMappingChecked: riskScoreMapping.length > 0, }, falsePositives, + investigationFields: investigationFields ?? [], threat: threat as Threats, threatIndicatorPath, }; @@ -343,6 +345,7 @@ const commonRuleParamsKeys = [ 'name', 'description', 'false_positives', + 'investigation_fields', 'rule_id', 'max_signals', 'risk_score', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 4232481eee861..c5b98b1bfd39d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -89,6 +89,7 @@ export interface AboutStepRule { riskScore: AboutStepRiskScore; references: string[]; falsePositives: string[]; + investigationFields: string[]; license: string; ruleNameOverride: string; tags: string[]; @@ -238,6 +239,7 @@ export interface AboutStepRuleJson { timestamp_override?: TimestampOverride; timestamp_override_fallback_disabled?: boolean; note?: string; + investigation_fields?: string[]; } export interface ScheduleStepRuleJson { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 791d8ae322260..5f54eea162c77 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -83,6 +83,7 @@ export const stepAboutDefaultValue: AboutStepRule = { isBuildingBlock: false, severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, riskScore: { value: 21, mapping: [], isMappingChecked: false }, + investigationFields: [], references: [''], falsePositives: [''], license: '', diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx index ee641e5c69184..6b52bcf921b0a 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx @@ -55,6 +55,7 @@ const contextValue: LeftPanelContext = { scopeId: '', browserFields: null, searchHit: undefined, + investigationFields: [], }; const renderCorrelationDetails = () => { diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx index 79ce5e8f4bf0d..28d05d3a08d70 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx @@ -10,6 +10,7 @@ import { type Criteria, EuiBasicTable, formatDate, EuiEmptyPrompt } from '@elast import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { isRight } from 'fp-ts/lib/Either'; +import { ALERT_REASON, ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { SeverityBadge } from '../../../detections/components/rules/severity_badge'; import { usePaginatedAlerts } from '../hooks/use_paginated_alerts'; import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations'; @@ -26,12 +27,12 @@ export const columns = [ render: (value: string) => formatDate(value, TIMESTAMP_DATE_FORMAT), }, { - field: 'kibana.alert.rule.name', + field: ALERT_RULE_NAME, name: i18n.CORRELATIONS_RULE_COLUMN_TITLE, truncateText: true, }, { - field: 'kibana.alert.reason', + field: ALERT_REASON, name: i18n.CORRELATIONS_REASON_COLUMN_TITLE, truncateText: true, }, diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx index 731bfeda95712..8382e13e6fcc6 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx @@ -12,12 +12,12 @@ import { EuiTitle, EuiSpacer, EuiInMemoryTable, - EuiHorizontalRule, EuiText, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiIcon, + EuiPanel, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { ExpandablePanel } from '../../shared/components/expandable_panel'; @@ -207,7 +207,7 @@ export const HostDetails: React.FC = ({ hostName, timestamp }) return ( <> -

      {i18n.HOSTS_TITLE}

      +

      {i18n.HOST_TITLE}

      = ({ hostName, timestamp }) /> )} - - - - -
      {i18n.RELATED_USERS_TITLE}
      -
      -
      - - - - - -
      - - + + + +
      {i18n.RELATED_USERS_TITLE}
      +
      +
      + + + + + +
      + + - - + setQuery={setQuery} + deleteQuery={deleteQuery} + refetch={refetchRelatedUsers} + > + + +
      +
      ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx index 70fc7554e64cd..504d2d2c9232f 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx @@ -108,7 +108,8 @@ const columns: Array> = [ * Prevalence table displayed in the document details expandable flyout left section under the Insights tab */ export const PrevalenceDetails: React.FC = () => { - const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } = useLeftPanelContext(); + const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId, investigationFields } = + useLeftPanelContext(); const data = useMemo(() => { const summaryRows = getSummaryRows({ @@ -116,6 +117,7 @@ export const PrevalenceDetails: React.FC = () => { data: dataFormattedForFieldBrowser || [], eventId, scopeId, + investigationFields, isReadOnly: false, }); @@ -137,7 +139,7 @@ export const PrevalenceDetails: React.FC = () => { userPrevalence: fields, }; }); - }, [browserFields, dataFormattedForFieldBrowser, eventId, scopeId]); + }, [browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId]); if (!eventId || !dataFormattedForFieldBrowser || !browserFields || !data || data.length === 0) { return ( diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts index b8c0122a7a595..a83d9911ad32c 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts @@ -21,8 +21,8 @@ export const SESSION_VIEW_ERROR_MESSAGE = i18n.translate( } ); -export const USERS_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.usersTitle', { - defaultMessage: 'Users', +export const USER_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.userTitle', { + defaultMessage: 'User', }); export const USERS_INFO_TITLE = i18n.translate( @@ -60,8 +60,8 @@ export const RELATED_ENTITIES_IP_COLUMN_TITLE = i18n.translate( } ); -export const HOSTS_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.hostsTitle', { - defaultMessage: 'Hosts', +export const HOST_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.hostTitle', { + defaultMessage: 'Host', }); export const HOSTS_INFO_TITLE = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx index ea55f811c341a..9e218c7ac94fb 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx @@ -12,12 +12,12 @@ import { EuiTitle, EuiSpacer, EuiInMemoryTable, - EuiHorizontalRule, EuiText, EuiIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip, + EuiPanel, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { ExpandablePanel } from '../../shared/components/expandable_panel'; @@ -208,7 +208,7 @@ export const UserDetails: React.FC = ({ userName, timestamp }) return ( <> -

      {i18n.USERS_TITLE}

      +

      {i18n.USER_TITLE}

      = ({ userName, timestamp }) /> )} - - - - -
      {i18n.RELATED_HOSTS_TITLE}
      -
      -
      - - - - - -
      - - + + + +
      {i18n.RELATED_HOSTS_TITLE}
      +
      +
      + + + + + +
      + + - - + setQuery={setQuery} + deleteQuery={deleteQuery} + refetch={refetchRelatedHosts} + > + + +
      +
      ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/context.tsx b/x-pack/plugins/security_solution/public/flyout/left/context.tsx index b552a830fc265..b5c4f340d5485 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/context.tsx @@ -15,12 +15,16 @@ import type { LeftPanelProps } from '.'; import type { GetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useTimelineEventsDetails } from '../../timelines/containers/details'; -import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; +import { + getAlertIndexAlias, + useBasicDataFromDetailsData, +} from '../../timelines/components/side_panel/event_details/helpers'; import { useSpaceId } from '../../common/hooks/use_space_id'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; import { SecurityPageName } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../common/containers/sourcerer'; +import { useRuleWithFallback } from '../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface LeftPanelContext { /** @@ -51,6 +55,10 @@ export interface LeftPanelContext { * The actual raw document object */ searchHit: SearchHit | undefined; + /** + * User defined fields to highlight (defined on the rule) + */ + investigationFields: string[]; /** * Retrieves searchHit values for the provided field */ @@ -83,6 +91,8 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane skip: !id, }); const getFieldsData = useGetFieldsData(searchHit?.fields); + const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const { rule: maybeRule } = useRuleWithFallback(ruleId); const contextValue = useMemo( () => @@ -95,6 +105,7 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane dataAsNestedObject, dataFormattedForFieldBrowser, searchHit, + investigationFields: maybeRule?.investigation_fields ?? [], getFieldsData, } : undefined, @@ -103,10 +114,11 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane indexName, scopeId, sourcererDataView.browserFields, - dataFormattedForFieldBrowser, - getFieldsData, dataAsNestedObject, + dataFormattedForFieldBrowser, searchHit, + maybeRule?.investigation_fields, + getFieldsData, ] ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts b/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts index 1a6387d4eb3f2..1e92ff0a6bd45 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts @@ -75,6 +75,7 @@ describe('useThreatIntelligenceDetails', () => { _index: 'testIndex', }, dataAsNestedObject: null, + investigationFields: [], }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts b/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts index 99bfc24bab50b..3569570568986 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts @@ -47,4 +47,5 @@ export const mockContextValue: LeftPanelContext = { dataAsNestedObject: { _id: 'testId', }, + investigationFields: [], }; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.test.tsx new file mode 100644 index 0000000000000..30076fd3ca1d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 { render } from '@testing-library/react'; +import { PreviewPanelContext } from '../context'; +import { mockContextValue } from '../mocks/mock_preview_panel_context'; +import { ALERT_REASON_PREVIEW_BODY_TEST_ID } from './test_ids'; +import { AlertReasonPreview } from './alert_reason_preview'; +import { ThemeProvider } from 'styled-components'; +import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; + +const mockTheme = getMockTheme({ eui: { euiFontSizeXS: '' } }); + +const panelContextValue = { + ...mockContextValue, +}; + +describe('', () => { + it('should render alert reason preview', () => { + const { getByTestId } = render( + + + + + + ); + expect(getByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).toBeInTheDocument(); + }); + + it('should render null is dataAsNestedObject is null', () => { + const contextValue = { + ...mockContextValue, + dataAsNestedObject: null, + }; + const { queryByTestId } = render( + + + + ); + expect(queryByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.tsx new file mode 100644 index 0000000000000..4fd912cbfbeec --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.tsx @@ -0,0 +1,50 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { ALERT_REASON_TITLE } from './translations'; +import { ALERT_REASON_PREVIEW_BODY_TEST_ID } from './test_ids'; +import { usePreviewPanelContext } from '../context'; +import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; + +/** + * Alert reason renderer on a preview panel on top of the right section of expandable flyout + */ +export const AlertReasonPreview: React.FC = () => { + const { dataAsNestedObject } = usePreviewPanelContext(); + + const renderer = useMemo( + () => + dataAsNestedObject != null + ? getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers }) + : null, + [dataAsNestedObject] + ); + + if (!dataAsNestedObject || !renderer) { + return null; + } + + return ( + + +
      {ALERT_REASON_TITLE}
      +
      + + {renderer.renderRow({ + contextId: 'event-details', + data: dataAsNestedObject, + isDraggable: false, + scopeId: 'global', + })} +
      + ); +}; + +AlertReasonPreview.displayName = 'AlertReasonPreview'; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts index 1c27fd7472fca..764ec90fd9bdf 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts @@ -38,3 +38,5 @@ export const RULE_PREVIEW_LOADING_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewLoadingSpinner'; export const RULE_PREVIEW_FOOTER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewFooter'; export const RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails'; +export const ALERT_REASON_PREVIEW_BODY_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutAlertReasonPreviewBody'; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts index e3f3b1fd095fb..36bfdd33ea2ca 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts @@ -31,3 +31,8 @@ export const RULE_PREVIEW_ACTIONS_TEXT = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.rulePreviewActionsSectionText', { defaultMessage: 'Actions' } ); + +export const ALERT_REASON_TITLE = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.alertReasonTitle', + { defaultMessage: 'Alert reason' } +); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/context.tsx b/x-pack/plugins/security_solution/public/flyout/preview/context.tsx index 521303635c25d..005ef1dcdb258 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/context.tsx @@ -7,11 +7,15 @@ import React, { createContext, useContext, useMemo } from 'react'; import type { DataViewBase } from '@kbn/es-query'; +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; import type { PreviewPanelProps } from '.'; -import { useRouteSpy } from '../../common/utils/route/use_route_spy'; -import { SecurityPageName } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../common/containers/sourcerer'; +import { useTimelineEventsDetails } from '../../timelines/containers/details'; +import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; +import { useSpaceId } from '../../common/hooks/use_space_id'; +import { useRouteSpy } from '../../common/utils/route/use_route_spy'; export interface PreviewPanelContext { /** @@ -34,6 +38,10 @@ export interface PreviewPanelContext { * Index pattern for rule details */ indexPattern: DataViewBase; + /** + * An object with top level fields from the ECS object + */ + dataAsNestedObject: Ecs | null; } export const PreviewPanelContext = createContext(undefined); @@ -52,12 +60,21 @@ export const PreviewPanelProvider = ({ ruleId, children, }: PreviewPanelProviderProps) => { + const currentSpaceId = useSpaceId(); + const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : ''; const [{ pageName }] = useRouteSpy(); const sourcererScope = pageName === SecurityPageName.detections ? SourcererScopeName.detections : SourcererScopeName.default; const sourcererDataView = useSourcererDataView(sourcererScope); + const [_, __, ___, dataAsNestedObject] = useTimelineEventsDetails({ + indexName: eventIndex, + eventId: id ?? '', + runtimeMappings: sourcererDataView.runtimeMappings, + skip: !id, + }); + const contextValue = useMemo( () => id && indexName && scopeId @@ -67,9 +84,10 @@ export const PreviewPanelProvider = ({ scopeId, ruleId: ruleId ?? '', indexPattern: sourcererDataView.indexPattern, + dataAsNestedObject, } : undefined, - [id, indexName, scopeId, ruleId, sourcererDataView.indexPattern] + [id, indexName, scopeId, ruleId, sourcererDataView.indexPattern, dataAsNestedObject] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/preview/index.tsx b/x-pack/plugins/security_solution/public/flyout/preview/index.tsx index 9bfefb8f257fd..db9f7bb5ba58a 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/index.tsx @@ -10,8 +10,9 @@ import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { panels } from './panels'; -export type PreviewPanelPaths = 'rule-preview'; +export type PreviewPanelPaths = 'rule-preview' | 'alert-reason-preview'; export const RulePreviewPanel: PreviewPanelPaths = 'rule-preview'; +export const AlertReasonPreviewPanel: PreviewPanelPaths = 'alert-reason-preview'; export const PreviewPanelKey: PreviewPanelProps['key'] = 'document-details-preview'; export interface PreviewPanelProps extends FlyoutPanelProps { diff --git a/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts b/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts index 4e9f9cc43d8ba..cdfe8ab5307ba 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { mockDataAsNestedObject } from '../../shared/mocks/mock_context'; import type { PreviewPanelContext } from '../context'; /** @@ -16,4 +18,5 @@ export const mockContextValue: PreviewPanelContext = { scopeId: 'scopeId', ruleId: '', indexPattern: { fields: [], title: 'test index' }, + dataAsNestedObject: mockDataAsNestedObject as unknown as Ecs, }; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx b/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx index b9aee26bdd577..e585c58945d9c 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; +import { AlertReasonPreview } from './components/alert_reason_preview'; import type { PreviewPanelPaths } from '.'; -import { RULE_PREVIEW } from './translations'; +import { ALERT_REASON_PREVIEW, RULE_PREVIEW } from './translations'; import { RulePreview } from './components/rule_preview'; import { RulePreviewFooter } from './components/rule_preview_footer'; @@ -40,4 +41,9 @@ export const panels: PreviewPanelType = [ content: , footer: , }, + { + id: 'alert-reason-preview', + name: ALERT_REASON_PREVIEW, + content: , + }, ]; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/translations.ts index 1db37fbb49bb8..cf359e7900cea 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/translations.ts @@ -11,3 +11,8 @@ export const RULE_PREVIEW = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.rulePreviewPanel', { defaultMessage: 'Rule preview' } ); + +export const ALERT_REASON_PREVIEW = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.alertReasonPreviewPanel', + { defaultMessage: 'Alert reason preview' } +); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/description.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/description.stories.tsx deleted file mode 100644 index 13fa592c4f7d6..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/description.stories.tsx +++ /dev/null @@ -1,92 +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 { css } from '@emotion/react'; -import type { Story } from '@storybook/react'; -import { Description } from './description'; -import { RightPanelContext } from '../context'; - -const ruleUuid = { - category: 'kibana', - field: 'kibana.alert.rule.uuid', - values: ['123'], - originalValue: ['123'], - isObjectArray: false, -}; -const ruleDescription = { - category: 'kibana', - field: 'kibana.alert.rule.description', - values: [ - `This is a very long description of the rule. In theory. this description is long enough that it should be cut off when displayed in collapsed mode. If it isn't then there is a problem`, - ], - originalValue: ['description'], - isObjectArray: false, -}; - -export default { - component: Description, - title: 'Flyout/Description', -}; - -const wrapper = (children: React.ReactNode, panelContextValue: RightPanelContext) => ( - -
      - {children} -
      -
      -); -export const Rule: Story = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleUuid, ruleDescription], - } as unknown as RightPanelContext; - - return wrapper(, panelContextValue); -}; - -export const Document: Story = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ - { - category: 'kibana', - field: 'kibana.alert.rule.description', - values: ['This is a description for the document.'], - originalValue: ['description'], - isObjectArray: false, - }, - ], - } as unknown as RightPanelContext; - - return wrapper(, panelContextValue); -}; - -export const EmptyDescription: Story = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ - ruleUuid, - { - category: 'kibana', - field: 'kibana.alert.rule.description', - values: [''], - originalValue: ['description'], - isObjectArray: false, - }, - ], - } as unknown as RightPanelContext; - - return wrapper(, panelContextValue); -}; - -export const Empty: Story = () => { - const panelContextValue = {} as unknown as RightPanelContext; - - return wrapper(, panelContextValue); -}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx index 1cf4823d79358..f80d7c1939661 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx @@ -8,14 +8,17 @@ import React from 'react'; import { render } from '@testing-library/react'; import { DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID } from './test_ids'; -import { DOCUMENT_DESCRIPTION_TITLE, RULE_DESCRIPTION_TITLE } from './translations'; +import { + DOCUMENT_DESCRIPTION_TITLE, + PREVIEW_RULE_DETAILS, + RULE_DESCRIPTION_TITLE, +} from './translations'; import { Description } from './description'; -import { TestProviders } from '../../../common/mock'; import { RightPanelContext } from '../context'; -import { ThemeProvider } from 'styled-components'; -import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); +import { mockGetFieldsData } from '../mocks/mock_context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { PreviewPanelKey } from '../../preview'; const ruleUuid = { category: 'kibana', @@ -41,23 +44,32 @@ const ruleName = { isObjectArray: false, }; -jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/components/link_to'); +const flyoutContextValue = { + openPreviewPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; + +const panelContextValue = (dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null) => + ({ + eventId: 'event id', + indexName: 'indexName', + scopeId: 'scopeId', + dataFormattedForFieldBrowser, + getFieldsData: mockGetFieldsData, + } as unknown as RightPanelContext); + +const renderDescription = (panelContext: RightPanelContext) => + render( + + + + + + ); describe('', () => { it('should render the component', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleUuid, ruleDescription, ruleName], - } as unknown as RightPanelContext; - - const { getByTestId } = render( - - - - - - - + const { getByTestId } = renderDescription( + panelContextValue([ruleUuid, ruleDescription, ruleName]) ); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); @@ -66,18 +78,8 @@ describe('', () => { }); it('should not render rule preview button if rule name is not available', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleUuid, ruleDescription], - } as unknown as RightPanelContext; - - const { getByTestId, queryByTestId } = render( - - - - - - - + const { getByTestId, queryByTestId } = renderDescription( + panelContextValue([ruleUuid, ruleDescription]) ); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); @@ -86,21 +88,44 @@ describe('', () => { }); it('should render document title if document is not an alert', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleDescription], - } as unknown as RightPanelContext; - - const { getByTestId } = render( - - - - - - - - ); + const { getByTestId } = renderDescription(panelContextValue([ruleDescription])); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent(DOCUMENT_DESCRIPTION_TITLE); }); + + it('should render null if dataFormattedForFieldBrowser is null', () => { + const panelContext = { + ...panelContextValue([ruleUuid, ruleDescription, ruleName]), + dataFormattedForFieldBrowser: null, + } as unknown as RightPanelContext; + + const { container } = renderDescription(panelContext); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should open preview panel when clicking on button', () => { + const panelContext = panelContextValue([ruleUuid, ruleDescription, ruleName]); + + const { getByTestId } = renderDescription(panelContext); + + getByTestId(RULE_SUMMARY_BUTTON_TEST_ID).click(); + + expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({ + id: PreviewPanelKey, + path: { tab: 'rule-preview' }, + params: { + id: panelContext.eventId, + indexName: panelContext.indexName, + scopeId: panelContext.scopeId, + banner: { + title: PREVIEW_RULE_DETAILS, + backgroundColor: 'warning', + textColor: 'warning', + }, + ruleId: ruleUuid.values[0], + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx index 010535ffc55c7..f4c24e64954ba 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx @@ -39,7 +39,7 @@ const renderHeader = (contextValue: RightPanelContext) => - + @@ -52,7 +52,7 @@ describe('', () => { jest.mocked(useAssistant).mockReturnValue({ showAssistant: true, promptContextId: '' }); }); - it('should render mitre attack information', () => { + it('should render component', () => { const contextValue = { dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, getFieldsData: jest.fn().mockImplementation(mockGetFieldsData), diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx index 23bf91053220a..eca086385eb17 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { VFC } from 'react'; import React, { memo } from 'react'; import { NewChatById } from '@kbn/elastic-assistant'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; @@ -26,10 +26,17 @@ import { PreferenceFormattedDate } from '../../../common/components/formatted_da import { FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids'; import { ShareButton } from './share_button'; +export interface HeaderTitleProps { + /** + * If false, update the margin-top to compensate the fact that the expand detail button is not displayed + */ + flyoutIsExpandable: boolean; +} + /** * Document details flyout right section header */ -export const HeaderTitle: FC = memo(() => { +export const HeaderTitle: VFC = memo(({ flyoutIsExpandable }) => { const { dataFormattedForFieldBrowser } = useRightPanelContext(); const { isAlert, ruleName, timestamp, alertUrl } = useBasicDataFromDetailsData( dataFormattedForFieldBrowser @@ -48,7 +55,7 @@ export const HeaderTitle: FC = memo(() => { justifyContent="flexEnd" gutterSize="none" css={css` - margin-top: -44px; + margin-top: ${flyoutIsExpandable ? '-44px' : '-28px'}; padding: 0 25px; `} > diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx index b156006a906ec..ed9d2cd623709 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx @@ -13,10 +13,16 @@ import { HighlightedFields } from './highlighted_fields'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; import { useHighlightedFields } from '../hooks/use_highlighted_fields'; import { TestProviders } from '../../../common/mock'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; jest.mock('../hooks/use_highlighted_fields'); +jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback'); describe('', () => { + beforeEach(() => { + (useRuleWithFallback as jest.Mock).mockReturnValue({ investigation_fields: [] }); + }); + it('should render the component', () => { const panelContextValue = { dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx index a0ef290b11ea8..f02682721ee5c 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx @@ -9,6 +9,8 @@ import type { FC } from 'react'; import React from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from '@elastic/eui'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; import { HighlightedFieldsCell } from './highlighted_fields_cell'; import { CellActionsMode, @@ -57,8 +59,13 @@ const columns: Array> = [ */ export const HighlightedFields: FC = () => { const { dataFormattedForFieldBrowser } = useRightPanelContext(); + const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const { rule: maybeRule } = useRuleWithFallback(ruleId); - const highlightedFields = useHighlightedFields({ dataFormattedForFieldBrowser }); + const highlightedFields = useHighlightedFields({ + dataFormattedForFieldBrowser, + investigationFields: maybeRule?.investigation_fields ?? [], + }); if (!dataFormattedForFieldBrowser || highlightedFields.length === 0) { return null; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx index a98246c3eaa0c..b017b4b9225a4 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx @@ -23,8 +23,14 @@ import { PREVALENCE_TAB_ID } from '../../left/components/prevalence_details'; * and the SummaryPanel component for data rendering. */ export const PrevalenceOverview: FC = () => { - const { eventId, indexName, browserFields, dataFormattedForFieldBrowser, scopeId } = - useRightPanelContext(); + const { + eventId, + indexName, + browserFields, + dataFormattedForFieldBrowser, + scopeId, + investigationFields, + } = useRightPanelContext(); const { openLeftPanel } = useExpandableFlyoutContext(); const goToCorrelationsTab = useCallback(() => { @@ -46,6 +52,7 @@ export const PrevalenceOverview: FC = () => { eventId, browserFields, dataFormattedForFieldBrowser, + investigationFields, scopeId, }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.stories.tsx deleted file mode 100644 index 68cb0b3a35e31..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.stories.tsx +++ /dev/null @@ -1,45 +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 { StorybookProviders } from '../../../common/mock/storybook_providers'; -import { Reason } from './reason'; -import { RightPanelContext } from '../context'; -import { mockDataAsNestedObject, mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; - -export default { - component: Reason, - title: 'Flyout/Reason', -}; - -export const Default: Story = () => { - const panelContextValue = { - dataAsNestedObject: mockDataAsNestedObject, - dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, - } as unknown as RightPanelContext; - - return ( - - - - - - ); -}; - -export const Empty: Story = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: {}, - } as unknown as RightPanelContext; - - return ( - - - - ); -}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx index b7050d1df0fa0..3ec854cfbd815 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx @@ -7,70 +7,85 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { REASON_TITLE_TEST_ID } from './test_ids'; +import { + REASON_DETAILS_PREVIEW_BUTTON_TEST_ID, + REASON_DETAILS_TEST_ID, + REASON_TITLE_TEST_ID, +} from './test_ids'; import { Reason } from './reason'; import { RightPanelContext } from '../context'; -import { mockDataAsNestedObject, mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; -import { euiDarkVars } from '@kbn/ui-theme'; -import { ThemeProvider } from 'styled-components'; +import { mockDataFormattedForFieldBrowser, mockGetFieldsData } from '../mocks/mock_context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { PreviewPanelKey } from '../../preview'; +import { PREVIEW_ALERT_REASON_DETAILS } from './translations'; -describe('', () => { - it('should render the component', () => { - const panelContextValue = { - dataAsNestedObject: mockDataAsNestedObject, - dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, - } as unknown as RightPanelContext; +const flyoutContextValue = { + openPreviewPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; - const { getByTestId } = render( - ({ eui: euiDarkVars, darkMode: true })}> - - - - - ); +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + scopeId: 'scopeId', + dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, + getFieldsData: mockGetFieldsData, +} as unknown as RightPanelContext; +const renderReason = (panelContext: RightPanelContext = panelContextValue) => + render( + + + + + + ); + +describe('', () => { + it('should render the component', () => { + const { getByTestId } = renderReason(); expect(getByTestId(REASON_TITLE_TEST_ID)).toBeInTheDocument(); }); it('should render null if dataFormattedForFieldBrowser is null', () => { - const panelContextValue = { - dataAsNestedObject: {}, + const panelContext = { + ...panelContextValue, + dataFormattedForFieldBrowser: null, } as unknown as RightPanelContext; - const { container } = render( - - - - ); + const { container } = renderReason(panelContext); expect(container).toBeEmptyDOMElement(); }); - it('should render null if dataAsNestedObject is null', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [], + it('should render no reason if the field is null', () => { + const panelContext = { + ...panelContextValue, + getFieldsData: () => {}, } as unknown as RightPanelContext; - const { container } = render( - - - - ); + const { getByTestId } = renderReason(panelContext); - expect(container).toBeEmptyDOMElement(); + expect(getByTestId(REASON_DETAILS_TEST_ID)).toBeEmptyDOMElement(); }); - it('should render null if renderer is null', () => { - const panelContextValue = { - dataAsNestedObject: {}, - dataFormattedForFieldBrowser: [], - } as unknown as RightPanelContext; - const { container } = render( - - - - ); + it('should open preview panel when clicking on button', () => { + const { getByTestId } = renderReason(); - expect(container).toBeEmptyDOMElement(); + getByTestId(REASON_DETAILS_PREVIEW_BUTTON_TEST_ID).click(); + + expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({ + id: PreviewPanelKey, + path: { tab: 'alert-reason-preview' }, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + scopeId: panelContextValue.scopeId, + banner: { + title: PREVIEW_ALERT_REASON_DETAILS, + backgroundColor: 'warning', + textColor: 'warning', + }, + }, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx index b6633ac42c46b..b356809917973 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx @@ -6,31 +6,71 @@ */ import type { FC } from 'react'; -import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import { REASON_DETAILS_TEST_ID, REASON_TITLE_TEST_ID } from './test_ids'; -import { ALERT_REASON_TITLE, DOCUMENT_REASON_TITLE } from './translations'; +import React, { useCallback, useMemo } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { getField } from '../../shared/utils'; +import { AlertReasonPreviewPanel, PreviewPanelKey } from '../../preview'; +import { + REASON_DETAILS_PREVIEW_BUTTON_TEST_ID, + REASON_DETAILS_TEST_ID, + REASON_TITLE_TEST_ID, +} from './test_ids'; +import { + ALERT_REASON_DETAILS_TEXT, + ALERT_REASON_TITLE, + DOCUMENT_REASON_TITLE, + PREVIEW_ALERT_REASON_DETAILS, +} from './translations'; import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; -import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; import { useRightPanelContext } from '../context'; /** * Displays the information provided by the rowRenderer. Supports multiple types of documents. */ export const Reason: FC = () => { - const { dataAsNestedObject, dataFormattedForFieldBrowser } = useRightPanelContext(); + const { eventId, indexName, scopeId, dataFormattedForFieldBrowser, getFieldsData } = + useRightPanelContext(); const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const alertReason = getField(getFieldsData(ALERT_REASON)); - const renderer = useMemo( - () => - dataAsNestedObject != null - ? getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers }) - : null, - [dataAsNestedObject] + const { openPreviewPanel } = useExpandableFlyoutContext(); + const openRulePreview = useCallback(() => { + openPreviewPanel({ + id: PreviewPanelKey, + path: { tab: AlertReasonPreviewPanel }, + params: { + id: eventId, + indexName, + scopeId, + banner: { + title: PREVIEW_ALERT_REASON_DETAILS, + backgroundColor: 'warning', + textColor: 'warning', + }, + }, + }); + }, [eventId, openPreviewPanel, indexName, scopeId]); + + const viewPreview = useMemo( + () => ( + + + {ALERT_REASON_DETAILS_TEXT} + + + ), + [openRulePreview] ); - if (!dataFormattedForFieldBrowser || !dataAsNestedObject || !renderer) { + if (!dataFormattedForFieldBrowser) { return null; } @@ -38,17 +78,21 @@ export const Reason: FC = () => { -
      {isAlert ? ALERT_REASON_TITLE : DOCUMENT_REASON_TITLE}
      +
      + {isAlert ? ( + + +
      {ALERT_REASON_TITLE}
      +
      + {viewPreview} +
      + ) : ( + DOCUMENT_REASON_TITLE + )} +
      - - {renderer.renderRow({ - contextId: 'event-details', - data: dataAsNestedObject, - isDraggable: false, - scopeId: 'global', - })} - + {alertReason}
      ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index 9e8b112851be6..7d41fe13f9fca 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -46,6 +46,8 @@ export const DESCRIPTION_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutDescriptionDetails'; export const REASON_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonTitle'; export const REASON_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonDetails'; +export const REASON_DETAILS_PREVIEW_BUTTON_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutReasonDetailsPreviewButton'; export const MITRE_ATTACK_TITLE_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackTitle'; export const MITRE_ATTACK_DETAILS_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackDetails'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index f32e9abb1d48f..a411b0f44054e 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -45,6 +45,13 @@ export const RULE_SUMMARY_TEXT = i18n.translate( } ); +export const ALERT_REASON_DETAILS_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.alertReasonDetailsText', + { + defaultMessage: 'Show full reason', + } +); + /* About section */ export const ABOUT_TITLE = i18n.translate( @@ -66,6 +73,11 @@ export const PREVIEW_RULE_DETAILS = i18n.translate( { defaultMessage: 'Preview rule details' } ); +export const PREVIEW_ALERT_REASON_DETAILS = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.previewAlertReasonDetailsText', + { defaultMessage: 'Preview alert reason' } +); + export const DOCUMENT_DESCRIPTION_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.documentDescriptionTitle', { diff --git a/x-pack/plugins/security_solution/public/flyout/right/content.tsx b/x-pack/plugins/security_solution/public/flyout/right/content.tsx index 9dd8391d24d11..d0d0b0a3b80b9 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/content.tsx @@ -10,23 +10,28 @@ import type { VFC } from 'react'; import React, { useMemo } from 'react'; import { FLYOUT_BODY_TEST_ID } from './test_ids'; import type { RightPanelPaths } from '.'; -import { tabs } from './tabs'; +import type { RightPanelTabsType } from './tabs'; +import {} from './tabs'; export interface PanelContentProps { /** * Id of the tab selected in the parent component to display its content */ selectedTabId: RightPanelPaths; + /** + * Tabs display right below the flyout's header + */ + tabs: RightPanelTabsType; } /** * Document details expandable flyout right section, that will display the content * of the overview, table and json tabs. */ -export const PanelContent: VFC = ({ selectedTabId }) => { +export const PanelContent: VFC = ({ selectedTabId, tabs }) => { const selectedTabContent = useMemo(() => { return tabs.find((tab) => tab.id === selectedTabId)?.content; - }, [selectedTabId]); + }, [selectedTabId, tabs]); return {selectedTabContent}; }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/context.tsx b/x-pack/plugins/security_solution/public/flyout/right/context.tsx index 7b12fc3a3a6cb..31eec77707d2f 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/context.tsx @@ -10,9 +10,13 @@ import { css } from '@emotion/react'; import React, { createContext, useContext, useMemo } from 'react'; import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; + import type { SearchHit } from '../../../common/search_strategy'; import { useTimelineEventsDetails } from '../../timelines/containers/details'; -import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; +import { + getAlertIndexAlias, + useBasicDataFromDetailsData, +} from '../../timelines/components/side_panel/event_details/helpers'; import { useSpaceId } from '../../common/hooks/use_space_id'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; import { SecurityPageName } from '../../../common/constants'; @@ -21,6 +25,7 @@ import { useSourcererDataView } from '../../common/containers/sourcerer'; import type { RightPanelProps } from '.'; import type { GetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; +import { useRuleWithFallback } from '../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface RightPanelContext { /** @@ -51,6 +56,10 @@ export interface RightPanelContext { * The actual raw document object */ searchHit: SearchHit | undefined; + /** + * User defined fields to highlight (defined on the rule) + */ + investigationFields: string[]; /** * Promise to trigger a data refresh */ @@ -94,6 +103,8 @@ export const RightPanelProvider = ({ skip: !id, }); const getFieldsData = useGetFieldsData(searchHit?.fields); + const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const { rule: maybeRule } = useRuleWithFallback(ruleId); const contextValue = useMemo( () => @@ -106,12 +117,14 @@ export const RightPanelProvider = ({ dataAsNestedObject, dataFormattedForFieldBrowser, searchHit, + investigationFields: maybeRule?.investigation_fields ?? [], refetchFlyoutData, getFieldsData, } : undefined, [ id, + maybeRule, indexName, scopeId, sourcererDataView.browserFields, diff --git a/x-pack/plugins/security_solution/public/flyout/right/header.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/header.test.tsx new file mode 100644 index 0000000000000..6432cbc2d41b6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/header.test.tsx @@ -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 React from 'react'; +import { render } from '@testing-library/react'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { TestProviders } from '../../common/mock'; +import { RightPanelContext } from './context'; +import { mockContextValue } from './mocks/mock_right_panel_context'; +import { PanelHeader } from './header'; +import { + COLLAPSE_DETAILS_BUTTON_TEST_ID, + EXPAND_DETAILS_BUTTON_TEST_ID, +} from './components/test_ids'; +import { mockFlyoutContextValue } from '../shared/mocks/mock_flyout_context'; + +describe('', () => { + it('should render expand details button if flyout is expandable', () => { + const { getByTestId } = render( + + + + window.alert('test')} + tabs={[]} + /> + + + + ); + + expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should not render expand details button if flyout is not expandable', () => { + const { queryByTestId } = render( + + + + window.alert('test')} + tabs={[]} + /> + + + + ); + + expect(queryByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/header.tsx b/x-pack/plugins/security_solution/public/flyout/right/header.tsx index 4f316b9a8be50..67425b6eb3565 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/header.tsx @@ -10,18 +10,32 @@ import type { VFC } from 'react'; import React, { memo } from 'react'; import { css } from '@emotion/react'; import type { RightPanelPaths } from '.'; -import { tabs } from './tabs'; +import type { RightPanelTabsType } from './tabs'; import { HeaderTitle } from './components/header_title'; import { ExpandDetailButton } from './components/expand_detail_button'; export interface PanelHeaderProps { + /** + * Id of the tab selected in the parent component to display its content + */ selectedTabId: RightPanelPaths; + /** + * Callback to set the selected tab id in the parent component + * @param selected + */ setSelectedTabId: (selected: RightPanelPaths) => void; - handleOnEventClosed?: () => void; + /** + * Tabs to display in the header + */ + tabs: RightPanelTabsType; + /** + * If true, the expand detail button will be displayed + */ + flyoutIsExpandable: boolean; } export const PanelHeader: VFC = memo( - ({ selectedTabId, setSelectedTabId, handleOnEventClosed }) => { + ({ flyoutIsExpandable, selectedTabId, setSelectedTabId, tabs }) => { const onSelectedTabChanged = (id: RightPanelPaths) => setSelectedTabId(id); const renderTabs = tabs.map((tab, index) => ( = memo( -
      - -
      + {flyoutIsExpandable && ( +
      + +
      + )} - + { if (!dataFormattedForFieldBrowser) return []; @@ -61,6 +66,7 @@ export const useHighlightedFields = ({ { category: 'kibana', field: ALERT_RULE_TYPE }, dataFormattedForFieldBrowser ); + const eventRuleType = Array.isArray(eventRuleTypeField?.originalValue) ? eventRuleTypeField?.originalValue?.[0] : eventRuleTypeField?.originalValue; @@ -69,6 +75,7 @@ export const useHighlightedFields = ({ eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride: investigationFields ?? [], }); return tableFields.reduce((acc, field) => { diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx index 162bc8bc851aa..5121e166e9a73 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx @@ -29,6 +29,10 @@ export interface UsePrevalenceParams { * Maintain backwards compatibility // TODO remove when possible */ scopeId: string; + /** + * User defined fields to highlight (defined on rule) + */ + investigationFields?: string[]; } /** @@ -41,6 +45,7 @@ export const usePrevalence = ({ eventId, browserFields, dataFormattedForFieldBrowser, + investigationFields, scopeId, }: UsePrevalenceParams): ReactElement[] => { // retrieves the highlighted fields @@ -49,11 +54,12 @@ export const usePrevalence = ({ getSummaryRows({ browserFields: browserFields || {}, data: dataFormattedForFieldBrowser || [], + investigationFields: investigationFields || [], eventId, scopeId, isReadOnly: false, }), - [browserFields, dataFormattedForFieldBrowser, eventId, scopeId] + [browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId] ); return useMemo( diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts index 72ca71badaf07..ac98ddd1df2b2 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; import { useRightPanelContext } from '../context'; @@ -14,8 +15,6 @@ const FIELD_USER_NAME = 'process.entry_leader.user.name' as const; const FIELD_USER_ID = 'process.entry_leader.user.id' as const; const FIELD_PROCESS_NAME = 'process.entry_leader.name' as const; const FIELD_START_AT = 'process.entry_leader.start' as const; -const FIELD_RULE_NAME = 'kibana.alert.rule.name' as const; -const FIELD_RULE_ID = 'kibana.alert.rule.uuid' as const; const FIELD_WORKING_DIRECTORY = 'process.group_leader.working_directory' as const; const FIELD_COMMAND = 'process.command_line' as const; @@ -48,8 +47,8 @@ export const useProcessData = () => { userName: getUserDisplayName(getFieldsData), processName: getField(getFieldsData(FIELD_PROCESS_NAME)), startAt: getField(getFieldsData(FIELD_START_AT)), - ruleName: getField(getFieldsData(FIELD_RULE_NAME)), - ruleId: getField(getFieldsData(FIELD_RULE_ID)), + ruleName: getField(getFieldsData(ALERT_RULE_NAME)), + ruleId: getField(getFieldsData(ALERT_RULE_UUID)), workdir: getField(getFieldsData(FIELD_WORKING_DIRECTORY)), command: getField(getFieldsData(FIELD_COMMAND)), }), diff --git a/x-pack/plugins/security_solution/public/flyout/right/index.tsx b/x-pack/plugins/security_solution/public/flyout/right/index.tsx index 80600b1357b60..1af4450b921ab 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/index.tsx @@ -9,6 +9,8 @@ import type { FC } from 'react'; import React, { memo, useMemo } from 'react'; import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { EventKind } from '../shared/hooks/use_fetch_field_value_pair_by_event_type'; +import { getField } from '../shared/utils'; import { useRightPanelContext } from './context'; import { PanelHeader } from './header'; import { PanelContent } from './content'; @@ -34,13 +36,17 @@ export interface RightPanelProps extends FlyoutPanelProps { */ export const RightPanel: FC> = memo(({ path }) => { const { openRightPanel } = useExpandableFlyoutContext(); - const { eventId, indexName, scopeId } = useRightPanelContext(); + const { eventId, getFieldsData, indexName, scopeId } = useRightPanelContext(); + + // for 8.10, we only render the flyout in its expandable mode if the document viewed is of type signal + const documentIsSignal = getField(getFieldsData('event.kind')) === EventKind.signal; + const tabsDisplayed = documentIsSignal ? tabs : tabs.filter((tab) => tab.id !== 'overview'); const selectedTabId = useMemo(() => { - const defaultTab = tabs[0].id; + const defaultTab = tabsDisplayed[0].id; if (!path) return defaultTab; - return tabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab; - }, [path]); + return tabsDisplayed.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab; + }, [path, tabsDisplayed]); const setSelectedTabId = (tabId: RightPanelTabsType[number]['id']) => { openRightPanel({ @@ -58,8 +64,13 @@ export const RightPanel: FC> = memo(({ path }) => { return ( <> - - + + ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts index 6ae872acd45ac..cdc058569d9d3 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils'; /** * Returns mocked data for field (mock this method: x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts) @@ -22,6 +22,8 @@ export const mockGetFieldsData = (field: string): string[] => { return ['host1']; case 'user.name': return ['user1']; + case ALERT_REASON: + return ['reason']; default: return []; } diff --git a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts index 95c986df43787..e7593b1eea9e9 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts @@ -20,5 +20,6 @@ export const mockContextValue: RightPanelContext = { browserFields: null, dataAsNestedObject: null, searchHit: undefined, + investigationFields: [], refetchFlyoutData: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts index 1ba754a747b07..987189ec2d722 100644 --- a/x-pack/plugins/security_solution/public/index.ts +++ b/x-pack/plugins/security_solution/public/index.ts @@ -6,6 +6,7 @@ */ import type { PluginInitializerContext } from '@kbn/core/public'; + import { Plugin } from './plugin'; import type { PluginSetup, PluginStart } from './types'; export type { TimelineModel } from './timelines/store/timeline/model'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx index cfbdcfd12c26e..041ecbf6f77d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx @@ -166,7 +166,7 @@ describe('Onboarding Component new section', () => { let render: () => ReturnType; beforeEach(() => { - mockedContext.startServices.upselling.registerSections({ + mockedContext.startServices.upselling.setSections({ endpointPolicyProtections: () =>
      {'pay up!'}
      , }); newPolicy = getMockNewPackage(); 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 7f204ca56d4ca..c055ed3281a09 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 @@ -66,7 +66,7 @@ describe('Endpoint Policy Settings Form', () => { describe('and when policy protections are not available', () => { beforeEach(() => { - upsellingService.registerSections({ + upsellingService.setSections({ endpointPolicyProtections: () =>
      {'pay up!'}
      , }); }); diff --git a/x-pack/plugins/security_solution/public/mocks.ts b/x-pack/plugins/security_solution/public/mocks.ts index 85b99420907ea..d40cda3294802 100644 --- a/x-pack/plugins/security_solution/public/mocks.ts +++ b/x-pack/plugins/security_solution/public/mocks.ts @@ -13,10 +13,11 @@ import type { PluginStart, PluginSetup } from './types'; const setupMock = (): PluginSetup => ({ resolver: jest.fn(), - upselling: new UpsellingService(), setAppLinksSwitcher: jest.fn(), }); +const upselling = new UpsellingService(); + const startMock = (): PluginStart => ({ getNavLinks$: jest.fn(() => new BehaviorSubject([])), setIsSidebarEnabled: jest.fn(), @@ -25,6 +26,7 @@ const startMock = (): PluginStart => ({ () => new BehaviorSubject({ leading: [], trailing: [] }) ), setExtraRoutes: jest.fn(), + getUpselling: () => upselling, }); export const securitySolutionMock = { diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index e330e233edd23..a9a2bbe6c7640 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -90,6 +90,7 @@ export const entityAnalyticsLinks: LinkItem = { path: ENTITY_ANALYTICS_PATH, capabilities: [`${SERVER_APP_ID}.show`], isBeta: false, + licenseType: 'platinum', globalSearchKeywords: [ENTITY_ANALYTICS], }; diff --git a/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx b/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx index 31cc7d0ae289c..8e7863b46ba3d 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/entity_analytics.tsx @@ -10,15 +10,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { EntityAnalyticsRiskScores } from '../components/entity_analytics/risk_score'; import { RiskScoreEntity } from '../../../common/search_strategy'; import { ENTITY_ANALYTICS } from '../../app/translations'; -import { Paywall } from '../../common/components/paywall'; -import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../app/types'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { HeaderPage } from '../../common/components/header_page'; import { LandingPageComponent } from '../../common/components/landing_page'; -import * as i18n from './translations'; import { EntityAnalyticsHeader } from '../components/entity_analytics/header'; import { EntityAnalyticsAnomalies } from '../components/entity_analytics/anomalies'; @@ -32,26 +29,20 @@ import { useHasSecurityCapability } from '../../helper_hooks'; const EntityAnalyticsComponent = () => { const { data: riskScoreEngineStatus } = useRiskEngineStatus(); const { indicesExist, loading: isSourcererLoading, indexPattern } = useSourcererDataView(); - const { isPlatinumOrTrialLicense, capabilitiesFetched } = useMlCapabilities(); - const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics'); - const isRiskScoreModuleLicenseAvailable = - isPlatinumOrTrialLicense && hasEntityAnalyticsCapability; + const isRiskScoreModuleLicenseAvailable = useHasSecurityCapability('entity-analytics'); return ( <> {indicesExist ? ( <> - {isPlatinumOrTrialLicense && capabilitiesFetched && ( - - - - )} + + + + - {!isPlatinumOrTrialLicense && capabilitiesFetched ? ( - - ) : isSourcererLoading ? ( + {isSourcererLoading ? ( ) : ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/translations.ts b/x-pack/plugins/security_solution/public/overview/pages/translations.ts index 5f44e18b53cd5..93d1d328c6177 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/pages/translations.ts @@ -104,13 +104,6 @@ export const DETECTION_RESPONSE_TITLE = i18n.translate( } ); -export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate( - 'xpack.securitySolution.entityAnalytics.pageDesc', - { - defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics', - } -); - export const TECHNICAL_PREVIEW = i18n.translate( 'xpack.securitySolution.entityAnalytics.technicalPreviewLabel', { diff --git a/x-pack/plugins/security_solution/public/plugin_contract.ts b/x-pack/plugins/security_solution/public/plugin_contract.ts index 77a08cda77c9b..06d72e736b041 100644 --- a/x-pack/plugins/security_solution/public/plugin_contract.ts +++ b/x-pack/plugins/security_solution/public/plugin_contract.ts @@ -40,7 +40,6 @@ export class PluginContract { public getSetupContract(): PluginSetup { return { resolver: lazyResolver, - upselling: this.upsellingService, setAppLinksSwitcher: (appLinksSwitcher) => { this.appLinksSwitcher = appLinksSwitcher; }, @@ -57,6 +56,7 @@ export class PluginContract { this.getStartedComponent$.next(getStartedComponent); }, getBreadcrumbsNav$: () => breadcrumbsNav$, + getUpselling: () => this.upsellingService, }; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index f1be0a3209ba1..8c070b6961b67 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -63,13 +63,19 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ scopeIdSelector(state, sourcererScope) ); useEffect(() => { + let ignore = false; const fetchAndSetDataView = async (dataViewId: string) => { const aDatView = await dataViews.get(dataViewId); + if (ignore) return; setDataView(aDatView); }; if (selectedDataViewId != null && !missingPatterns.length) { fetchAndSetDataView(selectedDataViewId); } + + return () => { + ignore = true; + }; }, [selectedDataViewId, missingPatterns, dataViews]); const openFieldEditor = useCallback( diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 0bcd156b6829c..6d2837db0eddc 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -214,6 +214,13 @@ export const useTimelineEventsHandler = ({ loadPage: wrappedLoadPage, updatedAt: 0, }); + + useEffect(() => { + if (timelineResponse.updatedAt !== 0) { + setUpdated(timelineResponse.updatedAt); + } + }, [setUpdated, timelineResponse.updatedAt]); + const { addWarning } = useAppToasts(); const timelineSearch = useCallback( @@ -252,7 +259,6 @@ export const useTimelineEventsHandler = ({ totalCount: response.totalCount, updatedAt: Date.now(), }; - setUpdated(newTimelineResponse.updatedAt); if (id === TimelineId.active) { activeTimeline.setExpandedDetail({}); activeTimeline.setPageName(pageName); @@ -336,7 +342,6 @@ export const useTimelineEventsHandler = ({ startTracking, data.search, dataViewId, - setUpdated, addWarning, refetchGrid, wrappedLoadPage, diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 8f920bb10f1b6..ce754bdc29307 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -169,7 +169,6 @@ export type StartServices = CoreStart & export interface PluginSetup { resolver: () => Promise; - upselling: UpsellingService; setAppLinksSwitcher: (appLinksSwitcher: AppLinksSwitcher) => void; } @@ -179,6 +178,7 @@ export interface PluginStart { setIsSidebarEnabled: (isSidebarEnabled: boolean) => void; setGetStartedPage: (getStartedComponent: React.ComponentType) => void; getBreadcrumbsNav$: () => Observable; + getUpselling: () => UpsellingService; } export interface AppObservableLibs { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 7c6d7926f2fa9..003e55fa6f1b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -100,4 +100,5 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({ namespace: undefined, data_view_id: undefined, alert_suppression: undefined, + investigation_fields: [], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts index b4c1a794929c5..02e162c0d7ff1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts @@ -44,6 +44,7 @@ describe('schedule_notification_actions', () => { responseActions: [], riskScore: 80, riskScoreMapping: [], + investigationFields: [], ruleNameOverride: undefined, dataViewId: undefined, outputIndex: 'output-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts index 701da673efcd6..a5381504e98fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts @@ -64,6 +64,7 @@ describe('schedule_throttle_notification_actions', () => { requiredFields: [], setup: '', alertSuppression: undefined, + investigationFields: undefined, }; }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts index 08a53c007dc06..7223b920c7bdc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts @@ -62,6 +62,7 @@ describe('duplicateRule', () => { timestampOverrideFallbackDisabled: undefined, dataViewId: undefined, alertSuppression: undefined, + investigationFields: undefined, }, schedule: { interval: '5m', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts index 66a4531d1c2e3..5248e7f06938f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts @@ -45,6 +45,7 @@ export const updateRules = async ({ ruleId: existingRule.params.ruleId, falsePositives: ruleUpdate.false_positives ?? [], from: ruleUpdate.from ?? 'now-6m', + investigationFields: ruleUpdate.investigation_fields ?? [], // Unlike the create route, immutable comes from the existing rule here immutable: existingRule.params.immutable, license: ruleUpdate.license, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 60e4d56278337..662085c95d62b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -136,6 +136,7 @@ describe('getExportAll', () => { note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), + investigation_fields: [], }); expect(detailsJson).toEqual({ exported_exception_list_count: 1, @@ -319,6 +320,7 @@ describe('getExportAll', () => { version: 1, revision: 0, exceptions_list: getListArrayMock(), + investigation_fields: [], }); expect(detailsJson).toEqual({ exported_exception_list_count: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index cf1c9db355bdd..b8f27c0d16a5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -132,6 +132,7 @@ describe('get_export_by_object_ids', () => { note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), + investigation_fields: [], }, exportDetails: { exported_exception_list_count: 0, @@ -327,6 +328,7 @@ describe('get_export_by_object_ids', () => { version: 1, revision: 0, exceptions_list: getListArrayMock(), + investigation_fields: [], }); expect(detailsJson).toEqual({ exported_exception_list_count: 0, @@ -523,6 +525,7 @@ describe('get_export_by_object_ids', () => { namespace: undefined, data_view_id: undefined, alert_suppression: undefined, + investigation_fields: [], }, ], }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts index 300d58ecc2e11..ecb379a1dcc2d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts @@ -409,6 +409,7 @@ export const convertPatchAPIToInternalSchema = ( description: nextParams.description ?? existingParams.description, ruleId: existingParams.ruleId, falsePositives: nextParams.false_positives ?? existingParams.falsePositives, + investigationFields: nextParams.investigation_fields ?? existingParams.investigationFields, from: nextParams.from ?? existingParams.from, immutable: existingParams.immutable, license: nextParams.license ?? existingParams.license, @@ -470,6 +471,7 @@ export const convertCreateAPIToInternalSchema = ( description: input.description, ruleId: newRuleId, falsePositives: input.false_positives ?? [], + investigationFields: input.investigation_fields ?? [], from: input.from ?? 'now-6m', immutable, license: input.license, @@ -619,6 +621,7 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { rule_name_override: params.ruleNameOverride, timestamp_override: params.timestampOverride, timestamp_override_fallback_disabled: params.timestampOverrideFallbackDisabled, + investigation_fields: params.investigationFields, author: params.author, false_positives: params.falsePositives, from: params.from, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts index ce60bf34d2ed8..d24bcaa5a7b9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts @@ -78,6 +78,7 @@ export const ruleOutput = (): RuleResponse => ({ data_view_id: undefined, saved_id: undefined, alert_suppression: undefined, + investigation_fields: [], }); describe('validate', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts index 44fa44ee01892..a6e6ee5282e89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts @@ -48,6 +48,7 @@ const getBaseRuleParams = (): BaseRuleParams => { timelineTitle: 'some-timeline-title', timestampOverride: undefined, timestampOverrideFallbackDisabled: undefined, + investigationFields: [], meta: { someMeta: 'someField', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index c851ed9288ea6..d3e66cf49148d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -57,6 +57,7 @@ import { RuleAuthorArray, RuleDescription, RuleFalsePositiveArray, + RuleCustomHighlightedFieldArray, RuleFilterArray, RuleLicense, RuleMetadata, @@ -97,6 +98,7 @@ export const baseRuleParams = t.exact( falsePositives: RuleFalsePositiveArray, from: RuleIntervalFrom, ruleId: RuleSignatureId, + investigationFields: t.union([RuleCustomHighlightedFieldArray, t.undefined]), immutable: IsRuleImmutable, license: t.union([RuleLicense, t.undefined]), outputIndex: AlertsIndex, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts index 93c93eb631fa2..6a522193558aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts @@ -523,6 +523,7 @@ export const sampleSignalHit = (): SignalHit => ({ filters: undefined, saved_id: undefined, alert_suppression: undefined, + investigation_fields: undefined, }, depth: 1, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index 06b0dc5b90514..8e06156bdc913 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -165,6 +165,7 @@ describe('buildAlert', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], query: 'user.name: root or user.name: admin', filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + investigation_fields: [], }, [ALERT_RULE_INDICES]: completeRule.ruleParams.index, ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { @@ -358,6 +359,7 @@ describe('buildAlert', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], query: 'user.name: root or user.name: admin', filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + investigation_fields: [], }, ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { actions: [], diff --git a/x-pack/plugins/security_solution_ess/kibana.jsonc b/x-pack/plugins/security_solution_ess/kibana.jsonc index cf1edb3f571c5..5e7e4584dc27d 100644 --- a/x-pack/plugins/security_solution_ess/kibana.jsonc +++ b/x-pack/plugins/security_solution_ess/kibana.jsonc @@ -9,7 +9,8 @@ "browser": true, "configPath": ["xpack", "securitySolutionEss"], "requiredPlugins": [ - "securitySolution" + "securitySolution", + "licensing", ], "optionalPlugins": [ "cloudExperiments", diff --git a/x-pack/plugins/security_solution/public/common/images/entity_paywall.png b/x-pack/plugins/security_solution_ess/public/common/images/entity_paywall.png similarity index 100% rename from x-pack/plugins/security_solution/public/common/images/entity_paywall.png rename to x-pack/plugins/security_solution_ess/public/common/images/entity_paywall.png diff --git a/x-pack/plugins/security_solution_ess/public/common/services.tsx b/x-pack/plugins/security_solution_ess/public/common/services.tsx index d00fd10350a7d..106782e337cc0 100644 --- a/x-pack/plugins/security_solution_ess/public/common/services.tsx +++ b/x-pack/plugins/security_solution_ess/public/common/services.tsx @@ -11,6 +11,7 @@ import { KibanaContextProvider, useKibana as useKibanaReact, } from '@kbn/kibana-react-plugin/public'; +import { NavigationProvider } from '@kbn/security-solution-navigation'; import type { SecuritySolutionEssPluginStartDeps } from '../types'; export type Services = CoreStart & SecuritySolutionEssPluginStartDeps; @@ -18,7 +19,11 @@ export type Services = CoreStart & SecuritySolutionEssPluginStartDeps; export const KibanaServicesProvider: React.FC<{ services: Services; }> = ({ services, children }) => { - return {children}; + return ( + + {children} + + ); }; export const useKibana = () => useKibanaReact(); @@ -29,3 +34,16 @@ export const createServices = ( ): Services => { return { ...core, ...pluginsStart }; }; + +export const withServicesProvider = ( + Component: React.ComponentType, + services: Services +) => { + return function WithServicesProvider(props: T) { + return ( + + + + ); + }; +}; diff --git a/x-pack/plugins/security_solution_ess/public/get_started/index.tsx b/x-pack/plugins/security_solution_ess/public/get_started/index.tsx index f85d20afe0c07..4b512fe2b9884 100644 --- a/x-pack/plugins/security_solution_ess/public/get_started/index.tsx +++ b/x-pack/plugins/security_solution_ess/public/get_started/index.tsx @@ -4,16 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import type React from 'react'; -import { KibanaServicesProvider, type Services } from '../common/services'; +import { withServicesProvider, type Services } from '../common/services'; import { GetStarted } from './lazy'; export const getSecurityGetStartedComponent = (services: Services): React.ComponentType => - function GetStartedComponent() { - return ( - - - - ); - }; + withServicesProvider(GetStarted, services); diff --git a/x-pack/plugins/security_solution_ess/public/plugin.ts b/x-pack/plugins/security_solution_ess/public/plugin.ts index 7224a46f682e4..a0c5aa694b73c 100644 --- a/x-pack/plugins/security_solution_ess/public/plugin.ts +++ b/x-pack/plugins/security_solution_ess/public/plugin.ts @@ -9,6 +9,7 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { subscribeBreadcrumbs } from './breadcrumbs'; import { createServices } from './common/services'; import { getSecurityGetStartedComponent } from './get_started'; +import { registerUpsellings } from './upselling/register_upsellings'; import type { SecuritySolutionEssPluginSetup, SecuritySolutionEssPluginStart, @@ -36,9 +37,13 @@ export class SecuritySolutionEssPlugin core: CoreStart, startDeps: SecuritySolutionEssPluginStartDeps ): SecuritySolutionEssPluginStart { - const { securitySolution } = startDeps; + const { securitySolution, licensing } = startDeps; const services = createServices(core, startDeps); + licensing.license$.subscribe((license) => { + registerUpsellings(securitySolution.getUpselling(), license, services); + }); + securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services)); subscribeBreadcrumbs(services); diff --git a/x-pack/plugins/security_solution_ess/public/types.ts b/x-pack/plugins/security_solution_ess/public/types.ts index 2bdeeffebf723..effa3750d3646 100644 --- a/x-pack/plugins/security_solution_ess/public/types.ts +++ b/x-pack/plugins/security_solution_ess/public/types.ts @@ -10,6 +10,7 @@ import type { PluginStart as SecuritySolutionPluginStart, } from '@kbn/security-solution-plugin/public'; import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SecuritySolutionEssPluginSetup {} @@ -24,4 +25,5 @@ export interface SecuritySolutionEssPluginSetupDeps { export interface SecuritySolutionEssPluginStartDeps { securitySolution: SecuritySolutionPluginStart; cloudExperiments?: CloudExperimentsPluginStart; + licensing: LicensingPluginStart; } diff --git a/x-pack/plugins/security_solution_ess/public/upselling/messages/investigation_guide_upselling.tsx b/x-pack/plugins/security_solution_ess/public/upselling/messages/investigation_guide_upselling.tsx new file mode 100644 index 0000000000000..8dd16883f5088 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/messages/investigation_guide_upselling.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const UPGRADE_INVESTIGATION_GUIDE = (requiredLicense: string) => + i18n.translate('xpack.securitySolutionEss.markdown.insight.upsell', { + defaultMessage: 'Upgrade to {requiredLicense} to make use of insights in investigation guides', + values: { + requiredLicense, + }, + }); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/pages/entity_analytics_upselling.tsx b/x-pack/plugins/security_solution_ess/public/upselling/pages/entity_analytics_upselling.tsx new file mode 100644 index 0000000000000..c7c4cd915fc03 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/entity_analytics_upselling.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { + EuiCard, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiTextColor, + EuiImage, + EuiPageHeader, + EuiSpacer, +} from '@elastic/eui'; + +import styled from '@emotion/styled'; +import { useNavigation } from '@kbn/security-solution-navigation'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import * as i18n from './translations'; +import paywallPng from '../../common/images/entity_paywall.png'; + +const PaywallDiv = styled.div` + max-width: 75%; + margin: 0 auto; + .euiCard__betaBadgeWrapper { + .euiCard__betaBadge { + width: auto; + } + } + .platinumCardDescription { + padding: 0 15%; + } +`; +const StyledEuiCard = styled(EuiCard)` + span.euiTitle { + max-width: 540px; + display: block; + margin: 0 auto; + } +`; + +const EntityAnalyticsUpsellingComponent = () => { + const { getAppUrl, navigateTo } = useNavigation(); + const subscriptionUrl = getAppUrl({ + appId: 'management', + path: 'stack/license_management', + }); + const goToSubscription = useCallback(() => { + navigateTo({ url: subscriptionUrl }); + }, [navigateTo, subscriptionUrl]); + return ( + + + + + + } + display="subdued" + title={ +

      + {i18n.ENTITY_ANALYTICS_LICENSE_DESC} +

      + } + description={false} + paddingSize="xl" + > + + + +

      + {i18n.UPGRADE_MESSAGE} +

      +
      + +
      + + {i18n.UPGRADE_BUTTON} + +
      +
      +
      +
      +
      + + + + + +
      +
      +
      + ); +}; + +EntityAnalyticsUpsellingComponent.displayName = 'EntityAnalyticsUpsellingComponent'; + +// eslint-disable-next-line import/no-default-export +export default React.memo(EntityAnalyticsUpsellingComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/paywall/translations.ts b/x-pack/plugins/security_solution_ess/public/upselling/pages/translations.ts similarity index 51% rename from x-pack/plugins/security_solution/public/common/components/paywall/translations.ts rename to x-pack/plugins/security_solution_ess/public/upselling/pages/translations.ts index a78fda1e90fa7..5ec6b838fbd46 100644 --- a/x-pack/plugins/security_solution/public/common/components/paywall/translations.ts +++ b/x-pack/plugins/security_solution_ess/public/upselling/pages/translations.ts @@ -7,14 +7,28 @@ import { i18n } from '@kbn/i18n'; -export const PLATINUM = i18n.translate('xpack.securitySolution.paywall.platinum', { +export const PLATINUM = i18n.translate('xpack.securitySolutionEss.paywall.platinum', { defaultMessage: 'Platinum', }); -export const UPGRADE_MESSAGE = i18n.translate('xpack.securitySolution.paywall.upgradeMessage', { +export const UPGRADE_MESSAGE = i18n.translate('xpack.securitySolutionEss.paywall.upgradeMessage', { defaultMessage: 'This feature is available with Platinum or higher subscription', }); -export const UPGRADE_BUTTON = i18n.translate('xpack.securitySolution.paywall.upgradeButton', { +export const UPGRADE_BUTTON = i18n.translate('xpack.securitySolutionEss.paywall.upgradeButton', { defaultMessage: 'Upgrade to Platinum', }); + +export const ENTITY_ANALYTICS_LICENSE_DESC = i18n.translate( + 'xpack.securitySolutionEss.entityAnalytics.pageDesc', + { + defaultMessage: 'Detect threats from users and hosts within your network with Entity Analytics', + } +); + +export const ENTITY_ANALYTICS_TITLE = i18n.translate( + 'xpack.securitySolutionEss.navigation.entityAnalytics', + { + defaultMessage: 'Entity Analytics', + } +); diff --git a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx new file mode 100644 index 0000000000000..f750507d17bc8 --- /dev/null +++ b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SecurityPageName } from '@kbn/security-solution-plugin/common'; +import type { UpsellingService } from '@kbn/security-solution-plugin/public'; +import type { + MessageUpsellings, + PageUpsellings, + SectionUpsellings, + UpsellingMessageId, + UpsellingSectionId, +} from '@kbn/security-solution-plugin/public/common/lib/upsellings/types'; +import type { ILicense, LicenseType } from '@kbn/licensing-plugin/public'; +import { lazy } from 'react'; +import type React from 'react'; +import { UPGRADE_INVESTIGATION_GUIDE } from './messages/investigation_guide_upselling'; +import type { Services } from '../common/services'; +import { withServicesProvider } from '../common/services'; +const EntityAnalyticsUpsellingLazy = lazy(() => import('./pages/entity_analytics_upselling')); + +interface UpsellingsConfig { + minimumLicenseRequired: LicenseType; + component: React.ComponentType; +} + +interface UpsellingsMessageConfig { + minimumLicenseRequired: LicenseType; + message: string; + id: UpsellingMessageId; +} + +type UpsellingPages = Array; +type UpsellingSections = Array; +type UpsellingMessages = UpsellingsMessageConfig[]; + +export const registerUpsellings = ( + upselling: UpsellingService, + license: ILicense, + services: Services +) => { + const upsellingPagesToRegister = upsellingPages.reduce( + (pageUpsellings, { pageName, minimumLicenseRequired, component }) => { + if (!license.hasAtLeast(minimumLicenseRequired)) { + pageUpsellings[pageName] = withServicesProvider(component, services); + } + return pageUpsellings; + }, + {} + ); + + const upsellingSectionsToRegister = upsellingSections.reduce( + (sectionUpsellings, { id, minimumLicenseRequired, component }) => { + if (!license.hasAtLeast(minimumLicenseRequired)) { + sectionUpsellings[id] = component; + } + return sectionUpsellings; + }, + {} + ); + + const upsellingMessagesToRegister = upsellingMessages.reduce( + (messagesUpsellings, { id, minimumLicenseRequired, message }) => { + if (!license.hasAtLeast(minimumLicenseRequired)) { + messagesUpsellings[id] = message; + } + return messagesUpsellings; + }, + {} + ); + + upselling.setPages(upsellingPagesToRegister); + upselling.setSections(upsellingSectionsToRegister); + upselling.setMessages(upsellingMessagesToRegister); +}; + +// Upsellings for entire pages, linked to a SecurityPageName +export const upsellingPages: UpsellingPages = [ + // It is highly advisable to make use of lazy loaded components to minimize bundle size. + { + pageName: SecurityPageName.entityAnalytics, + minimumLicenseRequired: 'platinum', + component: EntityAnalyticsUpsellingLazy, + }, +]; + +// Upsellings for sections, linked by arbitrary ids +export const upsellingSections: UpsellingSections = [ + // It is highly advisable to make use of lazy loaded components to minimize bundle size. +]; + +// Upsellings for sections, linked by arbitrary ids +export const upsellingMessages: UpsellingMessages = [ + { + id: 'investigation_guide', + minimumLicenseRequired: 'platinum', + message: UPGRADE_INVESTIGATION_GUIDE('platinum'), + }, +]; diff --git a/x-pack/plugins/security_solution_ess/tsconfig.json b/x-pack/plugins/security_solution_ess/tsconfig.json index c37fc77790254..08c7e49b6a166 100644 --- a/x-pack/plugins/security_solution_ess/tsconfig.json +++ b/x-pack/plugins/security_solution_ess/tsconfig.json @@ -19,5 +19,8 @@ "@kbn/i18n", "@kbn/cloud-experiments-plugin", "@kbn/kibana-react-plugin", + "@kbn/security-solution-navigation", + "@kbn/licensing-plugin", + "@kbn/shared-ux-page-kibana-template", ] } diff --git a/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx b/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx index 0a1d1e7693912..6468ebd602543 100644 --- a/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx +++ b/x-pack/plugins/security_solution_serverless/public/get_started/get_started.tsx @@ -9,8 +9,6 @@ import { EuiTitle, useEuiTheme, useEuiShadow } from '@elastic/eui'; import React from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { css } from '@emotion/react'; - -import { NavigationProvider } from '@kbn/security-solution-navigation'; import { WelcomePanel } from './welcome_panel'; import { TogglePanel } from './toggle_panel'; import { @@ -114,17 +112,15 @@ export const GetStartedComponent: React.FC = ({ productTypes }) padding: 0 ${euiTheme.base * 2.25}px; `} > - - - + ); diff --git a/x-pack/plugins/security_solution_serverless/public/plugin.ts b/x-pack/plugins/security_solution_serverless/public/plugin.ts index f16e7a97c617c..946e84c895f74 100644 --- a/x-pack/plugins/security_solution_serverless/public/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/public/plugin.ts @@ -40,7 +40,6 @@ export class SecuritySolutionServerlessPlugin _core: CoreSetup, setupDeps: SecuritySolutionServerlessPluginSetupDeps ): SecuritySolutionServerlessPluginSetup { - registerUpsellings(setupDeps.securitySolution.upselling, this.config.productTypes); setupDeps.securitySolution.setAppLinksSwitcher(projectAppLinksSwitcher); return {}; @@ -55,6 +54,7 @@ export class SecuritySolutionServerlessPlugin const services = createServices(core, startDeps); + registerUpsellings(securitySolution.getUpselling(), this.config.productTypes); securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes)); configureNavigation(services, this.config); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx new file mode 100644 index 0000000000000..a3640195544e0 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/public/upselling/lazy_upselling.tsx @@ -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 React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +const withSuspenseUpsell = ( + Component: React.ComponentType +): React.FC => + function WithSuspenseUpsell(props) { + return ( + }> + + + ); + }; + +export const ThreatIntelligencePaywallLazy = withSuspenseUpsell( + lazy(() => import('./pages/threat_intelligence_paywall')) +); + +export const OsqueryResponseActionsUpsellingSectionLazy = withSuspenseUpsell( + lazy(() => import('./pages/osquery_automated_response_actions')) +); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/investigation_guide_upselling.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/messages/investigation_guide_upselling.tsx similarity index 90% rename from x-pack/plugins/security_solution_serverless/public/upselling/pages/investigation_guide_upselling.tsx rename to x-pack/plugins/security_solution_serverless/public/upselling/messages/investigation_guide_upselling.tsx index 761a1426f1a07..591e979fbfbbe 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/pages/investigation_guide_upselling.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/messages/investigation_guide_upselling.tsx @@ -22,6 +22,3 @@ export const investigationGuideUpselling = (requiredPLI: AppFeatureKey): string const productTypeRequired = getProductTypeByPLI(requiredPLI); return productTypeRequired ? UPGRADE_INVESTIGATION_GUIDE(productTypeRequired) : ''; }; - -// eslint-disable-next-line import/no-default-export -export { investigationGuideUpselling as default }; diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx index 8ba6fc4a65981..6c886c681d0aa 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx @@ -31,58 +31,58 @@ describe('registerUpsellings', () => { it('should not register anything when all PLIs features are enabled', () => { mockGetProductAppFeatures.mockReturnValue(ALL_APP_FEATURE_KEYS); - const registerPages = jest.fn(); - const registerSections = jest.fn(); - const registerMessages = jest.fn(); + const setPages = jest.fn(); + const setSections = jest.fn(); + const setMessages = jest.fn(); const upselling = { - registerPages, - registerSections, - registerMessages, + setPages, + setSections, + setMessages, } as unknown as UpsellingService; registerUpsellings(upselling, allProductTypes); - expect(registerPages).toHaveBeenCalledTimes(1); - expect(registerPages).toHaveBeenCalledWith({}); + expect(setPages).toHaveBeenCalledTimes(1); + expect(setPages).toHaveBeenCalledWith({}); - expect(registerSections).toHaveBeenCalledTimes(1); - expect(registerSections).toHaveBeenCalledWith({}); + expect(setSections).toHaveBeenCalledTimes(1); + expect(setSections).toHaveBeenCalledWith({}); - expect(registerMessages).toHaveBeenCalledTimes(1); - expect(registerMessages).toHaveBeenCalledWith({}); + expect(setMessages).toHaveBeenCalledTimes(1); + expect(setMessages).toHaveBeenCalledWith({}); }); it('should register all upsellings pages, sections and messages when PLIs features are disabled', () => { mockGetProductAppFeatures.mockReturnValue([]); - const registerPages = jest.fn(); - const registerSections = jest.fn(); - const registerMessages = jest.fn(); + const setPages = jest.fn(); + const setSections = jest.fn(); + const setMessages = jest.fn(); const upselling = { - registerPages, - registerSections, - registerMessages, + setPages, + setSections, + setMessages, } as unknown as UpsellingService; registerUpsellings(upselling, allProductTypes); const expectedPagesObject = Object.fromEntries( - upsellingPages.map(({ pageName }) => [pageName, expect.any(Object)]) + upsellingPages.map(({ pageName }) => [pageName, expect.anything()]) ); - expect(registerPages).toHaveBeenCalledTimes(1); - expect(registerPages).toHaveBeenCalledWith(expectedPagesObject); + expect(setPages).toHaveBeenCalledTimes(1); + expect(setPages).toHaveBeenCalledWith(expectedPagesObject); const expectedSectionsObject = Object.fromEntries( - upsellingSections.map(({ id }) => [id, expect.any(Object)]) + upsellingSections.map(({ id }) => [id, expect.anything()]) ); - expect(registerSections).toHaveBeenCalledTimes(1); - expect(registerSections).toHaveBeenCalledWith(expectedSectionsObject); + expect(setSections).toHaveBeenCalledTimes(1); + expect(setSections).toHaveBeenCalledWith(expectedSectionsObject); const expectedMessagesObject = Object.fromEntries( upsellingMessages.map(({ id }) => [id, expect.any(String)]) ); - expect(registerMessages).toHaveBeenCalledTimes(1); - expect(registerMessages).toHaveBeenCalledWith(expectedMessagesObject); + expect(setMessages).toHaveBeenCalledTimes(1); + expect(setMessages).toHaveBeenCalledWith(expectedMessagesObject); }); }); diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx index 2997a39b544e1..01290dad9f00b 100644 --- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx +++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx @@ -15,37 +15,19 @@ import type { MessageUpsellings, UpsellingMessageId, } from '@kbn/security-solution-plugin/public/common/lib/upsellings/types'; -import React, { lazy } from 'react'; +import React from 'react'; import { EndpointPolicyProtectionsLazy } from './sections/endpoint_management'; import type { SecurityProductTypes } from '../../common/config'; import { getProductAppFeatures } from '../../common/pli/pli_features'; -import investigationGuideUpselling from './pages/investigation_guide_upselling'; - -const ThreatIntelligencePaywallLazy = lazy(async () => { - const ThreatIntelligencePaywall = (await import('./pages/threat_intelligence_paywall')).default; - - return { - default: () => , - }; -}); - -const OsqueryResponseActionsUpsellingSectionlLazy = lazy(async () => { - const OsqueryResponseActionsUpsellingSection = ( - await import('./pages/osquery_automated_response_actions') - ).default; - - return { - default: () => ( - - ), - }; -}); +import { investigationGuideUpselling } from './messages/investigation_guide_upselling'; +import { + OsqueryResponseActionsUpsellingSectionLazy, + ThreatIntelligencePaywallLazy, +} from './lazy_upselling'; interface UpsellingsConfig { pli: AppFeatureKey; - component: React.LazyExoticComponent; + component: React.ComponentType; } interface UpsellingsMessageConfig { @@ -94,42 +76,35 @@ export const registerUpsellings = ( {} ); - upselling.registerPages(upsellingPagesToRegister); - upselling.registerSections(upsellingSectionsToRegister); - upselling.registerMessages(upsellingMessagesToRegister); + upselling.setPages(upsellingPagesToRegister); + upselling.setSections(upsellingSectionsToRegister); + upselling.setMessages(upsellingMessagesToRegister); }; // Upsellings for entire pages, linked to a SecurityPageName export const upsellingPages: UpsellingPages = [ - // Sample code for registering a Upselling page - // Make sure the component is lazy loaded `const GenericUpsellingPageLazy = lazy(() => import('./pages/generic_upselling_page'));` - // { - // pageName: SecurityPageName.entityAnalytics, - // pli: AppFeatureKey.advancedInsights, - // component: () => , - // }, + // It is highly advisable to make use of lazy loaded components to minimize bundle size. { pageName: SecurityPageName.threatIntelligence, pli: AppFeatureKey.threatIntelligence, - component: ThreatIntelligencePaywallLazy, + component: () => ( + + ), }, ]; // Upsellings for sections, linked by arbitrary ids export const upsellingSections: UpsellingSections = [ - // Sample code for registering a Upselling section - // Make sure the component is lazy loaded `const GenericUpsellingSectionLazy = lazy(() => import('./pages/generic_upselling_section'));` - // { - // id: 'entity_analytics_panel', - // pli: AppFeatureKey.advancedInsights, - // component: () => , - // }, + // It is highly advisable to make use of lazy loaded components to minimize bundle size. { id: 'osquery_automated_response_actions', pli: AppFeatureKey.osqueryAutomatedResponseActions, - component: OsqueryResponseActionsUpsellingSectionlLazy, + component: () => ( + + ), }, - { id: 'endpointPolicyProtections', pli: AppFeatureKey.endpointPolicyProtections, diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/pages/generic_upselling_section.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/sections/generic_upselling_section.tsx similarity index 100% rename from x-pack/plugins/security_solution_serverless/public/upselling/pages/generic_upselling_section.tsx rename to x-pack/plugins/security_solution_serverless/public/upselling/sections/generic_upselling_section.tsx diff --git a/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts b/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts index c161be0bbfb78..3f0aafd563e9c 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/task_state_validation.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { v4 as uuidV4 } from 'uuid'; import { type TestElasticsearchUtils, type TestKibanaUtils, @@ -77,8 +78,8 @@ jest.mock('../queries/task_claiming', () => { const taskManagerStartSpy = jest.spyOn(TaskManagerPlugin.prototype, 'start'); describe('task state validation', () => { - // FLAKY: https://github.com/elastic/kibana/issues/161081 - describe.skip('allow_reading_invalid_state: true', () => { + describe('allow_reading_invalid_state: true', () => { + const taskIdsToRemove: string[] = []; let esServer: TestElasticsearchUtils; let kibanaServer: TestKibanaUtils; let taskManagerPlugin: TaskManagerStartContract; @@ -110,19 +111,20 @@ describe('task state validation', () => { }); afterEach(async () => { - await taskManagerPlugin.removeIfExists('foo'); + while (taskIdsToRemove.length > 0) { + const id = taskIdsToRemove.pop(); + await taskManagerPlugin.removeIfExists(id!); + } }); it('should drop unknown fields from the task state', async () => { - const taskRunnerPromise = new Promise((resolve) => { - mockTaskTypeRunFn.mockImplementation(() => { - setTimeout(resolve, 0); - return { state: {} }; - }); + mockTaskTypeRunFn.mockImplementation(() => { + return { state: {} }; }); + const id = uuidV4(); await injectTask(kibanaServer.coreStart.elasticsearch.client.asInternalUser, { - id: 'foo', + id, taskType: 'fooType', params: { foo: true }, state: { foo: 'test', bar: 'test', baz: 'test', invalidProperty: 'invalid' }, @@ -136,8 +138,11 @@ describe('task state validation', () => { retryAt: null, ownerId: null, }); + taskIdsToRemove.push(id); - await taskRunnerPromise; + await retry(async () => { + expect(mockTaskTypeRunFn).toHaveBeenCalled(); + }); expect(mockCreateTaskRunner).toHaveBeenCalledTimes(1); const call = mockCreateTaskRunner.mock.calls[0][0]; @@ -150,22 +155,21 @@ describe('task state validation', () => { it('should fail to update the task if the task runner returns an unknown property in the state', async () => { const errorLogSpy = jest.spyOn(pollingLifecycleOpts.logger, 'error'); - const taskRunnerPromise = new Promise((resolve) => { - mockTaskTypeRunFn.mockImplementation(() => { - setTimeout(resolve, 0); - return { state: { invalidField: true, foo: 'test', bar: 'test', baz: 'test' } }; - }); + mockTaskTypeRunFn.mockImplementation(() => { + return { state: { invalidField: true, foo: 'test', bar: 'test', baz: 'test' } }; }); - await taskManagerPlugin.schedule({ - id: 'foo', + const task = await taskManagerPlugin.schedule({ taskType: 'fooType', params: {}, state: { foo: 'test', bar: 'test', baz: 'test' }, schedule: { interval: '1d' }, }); + taskIdsToRemove.push(task.id); - await taskRunnerPromise; + await retry(async () => { + expect(mockTaskTypeRunFn).toHaveBeenCalled(); + }); expect(mockCreateTaskRunner).toHaveBeenCalledTimes(1); const call = mockCreateTaskRunner.mock.calls[0][0]; @@ -175,21 +179,19 @@ describe('task state validation', () => { baz: 'test', }); expect(errorLogSpy).toHaveBeenCalledWith( - 'Task fooType "foo" failed: Error: [invalidField]: definition for this key is missing', + `Task fooType "${task.id}" failed: Error: [invalidField]: definition for this key is missing`, expect.anything() ); }); it('should migrate the task state', async () => { - const taskRunnerPromise = new Promise((resolve) => { - mockTaskTypeRunFn.mockImplementation(() => { - setTimeout(resolve, 0); - return { state: {} }; - }); + mockTaskTypeRunFn.mockImplementation(() => { + return { state: {} }; }); + const id = uuidV4(); await injectTask(kibanaServer.coreStart.elasticsearch.client.asInternalUser, { - id: 'foo', + id, taskType: 'fooType', params: { foo: true }, state: {}, @@ -202,8 +204,11 @@ describe('task state validation', () => { retryAt: null, ownerId: null, }); + taskIdsToRemove.push(id); - await taskRunnerPromise; + await retry(async () => { + expect(mockTaskTypeRunFn).toHaveBeenCalled(); + }); expect(mockCreateTaskRunner).toHaveBeenCalledTimes(1); const call = mockCreateTaskRunner.mock.calls[0][0]; @@ -216,15 +221,13 @@ describe('task state validation', () => { it('should debug log by default when reading an invalid task state', async () => { const debugLogSpy = jest.spyOn(pollingLifecycleOpts.logger, 'debug'); - const taskRunnerPromise = new Promise((resolve) => { - mockTaskTypeRunFn.mockImplementation(() => { - setTimeout(resolve, 0); - return { state: {} }; - }); + mockTaskTypeRunFn.mockImplementation(() => { + return { state: {} }; }); + const id = uuidV4(); await injectTask(kibanaServer.coreStart.elasticsearch.client.asInternalUser, { - id: 'foo', + id, taskType: 'fooType', params: { foo: true }, state: { foo: true, bar: 'test', baz: 'test' }, @@ -238,8 +241,11 @@ describe('task state validation', () => { retryAt: null, ownerId: null, }); + taskIdsToRemove.push(id); - await taskRunnerPromise; + await retry(async () => { + expect(mockTaskTypeRunFn).toHaveBeenCalled(); + }); expect(mockCreateTaskRunner).toHaveBeenCalledTimes(1); const call = mockCreateTaskRunner.mock.calls[0][0]; @@ -250,12 +256,13 @@ describe('task state validation', () => { }); expect(debugLogSpy).toHaveBeenCalledWith( - `[fooType][foo] Failed to validate the task's state. Allowing read operation to proceed because allow_reading_invalid_state is true. Error: [foo]: expected value of type [string] but got [boolean]` + `[fooType][${id}] Failed to validate the task's state. Allowing read operation to proceed because allow_reading_invalid_state is true. Error: [foo]: expected value of type [string] but got [boolean]` ); }); }); describe('allow_reading_invalid_state: false', () => { + const taskIdsToRemove: string[] = []; let esServer: TestElasticsearchUtils; let kibanaServer: TestKibanaUtils; let taskManagerPlugin: TaskManagerStartContract; @@ -293,14 +300,18 @@ describe('task state validation', () => { }); afterEach(async () => { - await taskManagerPlugin.removeIfExists('foo'); + while (taskIdsToRemove.length > 0) { + const id = taskIdsToRemove.pop(); + await taskManagerPlugin.removeIfExists(id!); + } }); it('should fail the task run when setting allow_reading_invalid_state:false and reading an invalid state', async () => { const errorLogSpy = jest.spyOn(pollingLifecycleOpts.logger, 'error'); + const id = uuidV4(); await injectTask(kibanaServer.coreStart.elasticsearch.client.asInternalUser, { - id: 'foo', + id, taskType: 'fooType', params: { foo: true }, state: { foo: true, bar: 'test', baz: 'test' }, @@ -314,6 +325,7 @@ describe('task state validation', () => { retryAt: null, ownerId: null, }); + taskIdsToRemove.push(id); await retry(async () => { expect(errorLogSpy).toHaveBeenCalledWith( diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index ca55a8288dd12..61ddfa8e00505 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -6613,6 +6613,31 @@ } } } + }, + "alerts_stats": { + "type": "array", + "items": { + "properties": { + "posture_type": { + "type": "keyword" + }, + "rules_count": { + "type": "long" + }, + "alerts_count": { + "type": "long" + }, + "alerts_open_count": { + "type": "long" + }, + "alerts_closed_count": { + "type": "long" + }, + "alerts_acknowledged_count": { + "type": "long" + } + } + } } } }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2f997dec6acbd..9bef8ead17f65 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -507,7 +507,6 @@ "controls.rangeSlider.description": "Ajoutez un contrôle pour la sélection d'une plage de valeurs de champ.", "controls.rangeSlider.displayName": "Curseur de plage", "controls.rangeSlider.popover.noAvailableDataHelpText": "Il n'y a aucune donnée à afficher. Ajustez la plage temporelle et les filtres.", - "controls.rangeSlider.popover.noDataHelpText": "La plage sélectionnée n'a généré aucune donnée. Aucun filtre n'a été appliqué.", "controls.timeSlider.description": "Ajouter un curseur pour la sélection d'une plage temporelle", "controls.timeSlider.displayName": "Curseur temporel", "controls.timeSlider.nextLabel": "Fenêtre temporelle suivante", @@ -11588,7 +11587,6 @@ "xpack.csp.vulnerabilities.table.filterIn": "Inclure", "xpack.csp.vulnerabilities.table.filterOut": "Exclure", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle": "Pack", - "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle": "Ressource", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle": "Version", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.jsonTabLabel": "JSON", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.loadingAriaLabel": "Chargement", @@ -11615,7 +11613,6 @@ "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "Tout afficher", "xpack.csp.vulnerabilityTable.column.fixVersion": "Version du correctif", "xpack.csp.vulnerabilityTable.column.package": "Pack", - "xpack.csp.vulnerabilityTable.column.resource": "Ressource", "xpack.csp.vulnerabilityTable.column.severity": "Sévérité", "xpack.csp.vulnerabilityTable.column.sortAscending": "Basse -> Critique", "xpack.csp.vulnerabilityTable.column.sortDescending": "Critique -> Basse", @@ -14857,8 +14854,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.mysqlName": "MySQL", "xpack.enterpriseSearch.workplaceSearch.integrations.netowkrDriveDescription": "Effectuez des recherches sur le contenu de votre lecteur réseau avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveName": "Lecteur réseau", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription": "Effectuez des recherches dans vos fichiers stockés sur OneDrive avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName": "OneDrive", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleDescription": "Effectuez des recherches sur votre contenu sur Oracle avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleName": "Oracle", "xpack.enterpriseSearch.workplaceSearch.integrations.postgreSQLDescription": "Effectuez des recherches sur votre contenu sur PostgreSQL avec Enterprise Search.", @@ -14866,7 +14861,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.s3": "Amazon S3", "xpack.enterpriseSearch.workplaceSearch.integrations.s3Description": "Effectuez des recherches sur votre contenu sur Amazon S3 avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription": "Effectuez des recherches dans votre contenu sur Salesforce Sandbox avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName": "Sandbox Salesforce", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineDescription": "Effectuez des recherches dans vos fichiers stockés sur SharePoint Online avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Effectuez des recherches dans vos fichiers stockés sur le serveur Microsoft SharePoint avec Workplace Search.", @@ -31108,7 +31102,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "Sélectionner un champ pour vérifier la cardinalité", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "Seuil", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "La suppression d'alertes est activée avec la licence Platinum ou supérieure", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "Sélectionner un champ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "Supprimer les alertes pour", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "Supprimer les alertes par", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "Facultatif (version d'évaluation technique)", @@ -32982,7 +32975,6 @@ "xpack.securitySolution.entityAnalytics.header.criticalUsers": "Utilisateurs critiques", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "Le tableau des risques de l'hôte n'est pas affecté par la plage temporelle. Ce tableau montre le dernier score de risque enregistré pour chaque hôte.", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "Scores de risque de l'hôte", - "xpack.securitySolution.entityAnalytics.pageDesc": "Détecter les menaces des utilisateurs et des hôtes de votre réseau avec l'Analyse des entités", "xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "Le panneau de Score de risque de l'hôte affiche la liste des hôtes à risque ainsi que leur dernier score de risque. Vous pouvez filtrer cette liste à l’aide de filtres globaux dans la barre de recherche KQL. Le filtre de sélecteur de plage temporelle affiche les alertes dans l’intervalle de temps sélectionné uniquement et ne filtre pas la liste des hôtes à risque.", "xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "En savoir plus", "xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "En version d'évaluation technique", @@ -33366,7 +33358,6 @@ "xpack.securitySolution.flyout.entities.failRelatedHostsDescription": "Impossible de lancer la recherche sur les hôtes associés", "xpack.securitySolution.flyout.entities.failRelatedUsersDescription": "Impossible de lancer la recherche sur les utilisateurs associés", "xpack.securitySolution.flyout.entities.hostsInfoTitle": "Informations sur l’hôte", - "xpack.securitySolution.flyout.entities.hostsTitle": "Hôtes", "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "Adresses IP", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "Nom", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "Hôtes associés", @@ -33374,7 +33365,6 @@ "xpack.securitySolution.flyout.entities.relatedUsersTitle": "Utilisateurs associés", "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "Ces utilisateurs ont été authentifiés avec succès sur l’hôte concerné après l’alerte.", "xpack.securitySolution.flyout.entities.usersInfoTitle": "Informations sur l’utilisateur", - "xpack.securitySolution.flyout.entities.usersTitle": "Utilisateurs", "xpack.securitySolution.flyout.prevalenceErrorMessage": "prévalence", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "Nombre d'alertes", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "Compte du document", @@ -33672,7 +33662,6 @@ "xpack.securitySolution.markdown.insight.relativeTimerange": "Plage temporelle relative", "xpack.securitySolution.markdown.insight.relativeTimerangeText": "Sélectionnez une plage horaire pour limiter la requête, par rapport à l'heure de création de l'alerte (facultatif).", "xpack.securitySolution.markdown.insight.title": "Examiner", - "xpack.securitySolution.markdown.insight.upsell": "Mettez à niveau vers Platinum pour pouvoir utiliser les informations exploitables dans des guides d’investigation", "xpack.securitySolution.markdown.invalid": "Markdown non valide détecté", "xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "Ajouter une recherche", "xpack.securitySolution.markdown.osquery.addModalTitle": "Ajouter une recherche", @@ -33956,9 +33945,6 @@ "xpack.securitySolution.paginatedTable.showingSubtitle": "Affichant", "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "Affiner votre recherche pour mieux filtrer les résultats", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - trop de résultats", - "xpack.securitySolution.paywall.platinum": "Platinum", - "xpack.securitySolution.paywall.upgradeButton": "Mettre à niveau vers Platinum", - "xpack.securitySolution.paywall.upgradeMessage": "Cette fonctionnalité est disponible avec l'abonnement Platinum ou supérieur", "xpack.securitySolution.policiesTab": "Politiques", "xpack.securitySolution.policy.backToPolicyList": "Retour à la liste des politiques", "xpack.securitySolution.policy.list.createdAt": "Date de création", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 81bdfd0f3ba59..b22c36e25a649 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -507,7 +507,6 @@ "controls.rangeSlider.description": "フィールド値の範囲を選択するためのコントロールを追加", "controls.rangeSlider.displayName": "範囲スライダー", "controls.rangeSlider.popover.noAvailableDataHelpText": "表示するデータがありません。時間範囲とフィルターを調整します。", - "controls.rangeSlider.popover.noDataHelpText": "選択された範囲にはデータがありません。フィルターが適用されませんでした。", "controls.timeSlider.description": "時間範囲を選択するためのスライダーを追加", "controls.timeSlider.displayName": "時間スライダー", "controls.timeSlider.nextLabel": "次の時間ウィンドウ", @@ -11603,7 +11602,6 @@ "xpack.csp.vulnerabilities.table.filterIn": "フィルタリング", "xpack.csp.vulnerabilities.table.filterOut": "除外", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle": "パッケージ", - "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle": "リソース", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle": "バージョン", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.jsonTabLabel": "JSON", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.loadingAriaLabel": "読み込み中", @@ -11630,7 +11628,6 @@ "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "すべて表示", "xpack.csp.vulnerabilityTable.column.fixVersion": "修正バージョン", "xpack.csp.vulnerabilityTable.column.package": "パッケージ", - "xpack.csp.vulnerabilityTable.column.resource": "リソース", "xpack.csp.vulnerabilityTable.column.severity": "深刻度", "xpack.csp.vulnerabilityTable.column.sortAscending": "低 -> 重大", "xpack.csp.vulnerabilityTable.column.sortDescending": "重大 -> 低", @@ -14871,8 +14868,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.mysqlName": "MySQL", "xpack.enterpriseSearch.workplaceSearch.integrations.netowkrDriveDescription": "エンタープライズ サーチでネットワークドライブコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveName": "ネットワークドライブ", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription": "Workplace Searchを使用して、OneDriveに保存されたファイルを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName": "OneDrive", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleDescription": "エンタープライズ サーチでOracleのコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleName": "Oracle", "xpack.enterpriseSearch.workplaceSearch.integrations.postgreSQLDescription": "エンタープライズ サーチでPostgreSQLのコンテンツを検索します。", @@ -14880,7 +14875,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.s3": "Amazon S3", "xpack.enterpriseSearch.workplaceSearch.integrations.s3Description": "エンタープライズサーチでAmazon S3のコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription": "Workplace Searchを使用して、Salesforce Sandboxのコンテンツを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName": "Salesforce Sandbox", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineDescription": "Workplace Searchを使用して、SharePointに保存されたファイルを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Workplace Searchを使用して、Microsoft SharePoint Serverに保存されたファイルを検索します。", @@ -31107,7 +31101,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "カーディナリティを確認するフィールドを選択します", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "しきい値", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "アラートの非表示は、プラチナライセンス以上で有効です", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "フィールドを選択", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "アラートを非表示", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "アラートを非表示", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "任意(テクニカルプレビュー)", @@ -32981,7 +32974,6 @@ "xpack.securitySolution.entityAnalytics.header.criticalUsers": "重要なユーザー", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "ホストリスク表は時間範囲の影響を受けません。この表は、各ホストの最後に記録されたリスクスコアを示します。", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "ホストリスクスコア", - "xpack.securitySolution.entityAnalytics.pageDesc": "Entity Analyticsを使用して、ネットワーク内のユーザーとホストから脅威を検出", "xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "ホストリスクスコアパネルには、リスクのあるホストの一覧と最新のリスクスコアが表示されます。KQL検索バーのグローバルフィルターを使って、この一覧をフィルタリングできます。時間範囲ピッカーフィルターは、選択した時間範囲内のアラートのみを表示し、リスクのあるホストの一覧をフィルタリングしません。", "xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "詳細", "xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "テクニカルプレビュー", @@ -33365,7 +33357,6 @@ "xpack.securitySolution.flyout.entities.failRelatedHostsDescription": "関連するホストで検索を実行できませんでした", "xpack.securitySolution.flyout.entities.failRelatedUsersDescription": "関連するユーザーで検索を実行できませんでした", "xpack.securitySolution.flyout.entities.hostsInfoTitle": "ホスト情報", - "xpack.securitySolution.flyout.entities.hostsTitle": "ホスト", "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "IPアドレス", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "名前", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "関連するホスト", @@ -33373,7 +33364,6 @@ "xpack.securitySolution.flyout.entities.relatedUsersTitle": "関連するユーザー", "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "アラート後、ユーザーは影響を受けるホストへの認証に成功しました。", "xpack.securitySolution.flyout.entities.usersInfoTitle": "ユーザー情報", - "xpack.securitySolution.flyout.entities.usersTitle": "ユーザー", "xpack.securitySolution.flyout.prevalenceErrorMessage": "発生率", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "アラート件数", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "ドキュメントカウント", @@ -33671,7 +33661,6 @@ "xpack.securitySolution.markdown.insight.relativeTimerange": "相対的時間範囲", "xpack.securitySolution.markdown.insight.relativeTimerangeText": "アラートの作成日時に相対的な、クエリを限定するための時間範囲を選択します(任意)。", "xpack.securitySolution.markdown.insight.title": "調査", - "xpack.securitySolution.markdown.insight.upsell": "プラチナにアップグレードして、調査ガイドのインサイトを利用", "xpack.securitySolution.markdown.invalid": "無効なマークダウンが検出されました", "xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "クエリを追加", "xpack.securitySolution.markdown.osquery.addModalTitle": "クエリを追加", @@ -33955,9 +33944,6 @@ "xpack.securitySolution.paginatedTable.showingSubtitle": "表示中", "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "クエリ範囲を縮めて結果をさらにフィルタリングしてください", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 結果が多すぎます", - "xpack.securitySolution.paywall.platinum": "プラチナ", - "xpack.securitySolution.paywall.upgradeButton": "プラチナにアップグレード", - "xpack.securitySolution.paywall.upgradeMessage": "この機能は、プラチナ以上のサブスクリプションでご利用いただけます", "xpack.securitySolution.policiesTab": "ポリシー", "xpack.securitySolution.policy.backToPolicyList": "ポリシーリストに戻る", "xpack.securitySolution.policy.list.createdAt": "作成日", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fe6dc047c1a3d..89dec8049136f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -507,7 +507,6 @@ "controls.rangeSlider.description": "添加用于选择字段值范围的控件。", "controls.rangeSlider.displayName": "范围滑块", "controls.rangeSlider.popover.noAvailableDataHelpText": "没有可显示的数据。调整时间范围和筛选。", - "controls.rangeSlider.popover.noDataHelpText": "选定范围未生成任何数据。未应用任何筛选。", "controls.timeSlider.description": "添加用于选择时间范围的滑块", "controls.timeSlider.displayName": "时间滑块", "controls.timeSlider.nextLabel": "下一时间窗口", @@ -11603,7 +11602,6 @@ "xpack.csp.vulnerabilities.table.filterIn": "筛选范围", "xpack.csp.vulnerabilities.table.filterOut": "筛除", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle": "软件包", - "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle": "资源", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle": "版本", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.jsonTabLabel": "JSON", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.loadingAriaLabel": "正在加载", @@ -11630,7 +11628,6 @@ "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "查看全部", "xpack.csp.vulnerabilityTable.column.fixVersion": "修复版本", "xpack.csp.vulnerabilityTable.column.package": "软件包", - "xpack.csp.vulnerabilityTable.column.resource": "资源", "xpack.csp.vulnerabilityTable.column.severity": "严重性", "xpack.csp.vulnerabilityTable.column.sortAscending": "低 -> 严重", "xpack.csp.vulnerabilityTable.column.sortDescending": "严重 -> 低", @@ -14871,8 +14868,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.mysqlName": "MySQL", "xpack.enterpriseSearch.workplaceSearch.integrations.netowkrDriveDescription": "使用 Enterprise Search 搜索您的网络驱动器内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveName": "网络驱动器", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription": "通过 Workplace Search 搜索存储在 OneDrive 上的文件。", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName": "OneDrive", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleDescription": "使用 Enterprise Search 在 Oracle 上搜索您的内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleName": "Oracle", "xpack.enterpriseSearch.workplaceSearch.integrations.postgreSQLDescription": "使用 Enterprise Search 在 PostgreSQL 上搜索您的内容。", @@ -14880,7 +14875,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.s3": "Amazon S3", "xpack.enterpriseSearch.workplaceSearch.integrations.s3Description": "使用 Enterprise Search 在 Amazon S3 上搜索您的内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription": "通过 Workplace Search 搜索 Salesforce Sandbox 上的内容。", - "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName": "Salesforce Sandbox", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineDescription": "通过 Workplace Search 搜索存储在 SharePoint Online 上的文件。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "Sharepoint", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "通过 Workplace Search 搜索存储在 Microsoft SharePoint Server 上的文件。", @@ -31103,7 +31097,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "选择字段以检查基数", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "阈值", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "告警阻止通过白金级或更高级许可证启用", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "选择字段", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "阻止以下项的告警", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "阻止告警的依据", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "可选(技术预览)", @@ -32977,7 +32970,6 @@ "xpack.securitySolution.entityAnalytics.header.criticalUsers": "关键用户", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.hostsTableTooltip": "主机风险表不受时间范围影响。本表显示每台主机最新记录的风险分数。", "xpack.securitySolution.entityAnalytics.hostsRiskDashboard.title": "主机风险分数", - "xpack.securitySolution.entityAnalytics.pageDesc": "通过实体分析检测来自您网络中用户和主机的威胁", "xpack.securitySolution.entityAnalytics.riskDashboard.hostsTableTooltip": "“主机风险分数”面板显示有风险主机及其最新风险分数的列表。可以在 KQL 搜索栏中使用全局筛选来筛选此列表。时间范围选取器筛选将仅显示选定时间范围内的告警,并且不筛选有风险主机列表。", "xpack.securitySolution.entityAnalytics.riskDashboard.learnMore": "了解详情", "xpack.securitySolution.entityAnalytics.riskDashboard.tableTooltipTitle": "处于技术预览状态", @@ -33361,7 +33353,6 @@ "xpack.securitySolution.flyout.entities.failRelatedHostsDescription": "无法对相关主机执行搜索", "xpack.securitySolution.flyout.entities.failRelatedUsersDescription": "无法对相关用户执行搜索", "xpack.securitySolution.flyout.entities.hostsInfoTitle": "主机信息", - "xpack.securitySolution.flyout.entities.hostsTitle": "主机", "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "IP 地址", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "名称", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "相关主机", @@ -33369,7 +33360,6 @@ "xpack.securitySolution.flyout.entities.relatedUsersTitle": "相关用户", "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "告警后,这些用户已成功通过受影响主机的身份验证。", "xpack.securitySolution.flyout.entities.usersInfoTitle": "用户信息", - "xpack.securitySolution.flyout.entities.usersTitle": "用户", "xpack.securitySolution.flyout.prevalenceErrorMessage": "普及率", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "告警计数", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "文档计数", @@ -33667,7 +33657,6 @@ "xpack.securitySolution.markdown.insight.relativeTimerange": "相对时间范围", "xpack.securitySolution.markdown.insight.relativeTimerangeText": "选择相对于告警创建时间的时间范围(可选)以限制查询。", "xpack.securitySolution.markdown.insight.title": "调查", - "xpack.securitySolution.markdown.insight.upsell": "升级到白金级以利用调查指南中的洞见", "xpack.securitySolution.markdown.invalid": "检测到无效 Markdown", "xpack.securitySolution.markdown.osquery.addModalConfirmButtonLabel": "添加查询", "xpack.securitySolution.markdown.osquery.addModalTitle": "添加查询", @@ -33951,9 +33940,6 @@ "xpack.securitySolution.paginatedTable.showingSubtitle": "正在显示", "xpack.securitySolution.paginatedTable.tooManyResultsToastText": "缩减您的查询范围,以更好地筛选结果", "xpack.securitySolution.paginatedTable.tooManyResultsToastTitle": " - 结果过多", - "xpack.securitySolution.paywall.platinum": "白金级", - "xpack.securitySolution.paywall.upgradeButton": "升级到白金级", - "xpack.securitySolution.paywall.upgradeMessage": "白金级或更高级订阅可以使用此功能", "xpack.securitySolution.policiesTab": "策略", "xpack.securitySolution.policy.backToPolicyList": "返回到策略列表", "xpack.securitySolution.policy.list.createdAt": "创建日期", diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 69d84e39b10f2..e47c434860528 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -333,6 +333,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ] : []), '--notifications.connectors.default.email=notification-email', + '--xpack.task_manager.allow_reading_invalid_state=false', ], }, }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 66e075792b6fa..57c9651bec55a 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -76,6 +76,7 @@ export default ({ getService }: FtrProviderContext) => { author: [], created_by: 'elastic', description: 'Simple Rule Query', + investigation_fields: [], enabled: true, false_positives: [], from: 'now-6m', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts index fe17a9fb62008..b624cd95787aa 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts @@ -207,6 +207,7 @@ export default ({ getService }: FtrProviderContext) => { risk_score_mapping: [], name: 'Simple Rule Query', query: 'user.name: root or user.name: admin', + investigation_fields: [], references: [], related_integrations: [], required_fields: [], diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts index 49cf3cc5107a7..cb47021ba3d5e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts @@ -742,5 +742,6 @@ function expectToMatchRuleSchema(obj: RuleResponse): void { index: expect.arrayContaining([]), query: expect.any(String), actions: expect.arrayContaining([]), + investigation_fields: expect.arrayContaining([]), }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts index b671c2ce39d4d..9a55755f2e93a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts @@ -426,6 +426,28 @@ export default ({ getService }: FtrProviderContext) => { message: 'rule_id: "fake_id" not found', }); }); + + describe('investigation_fields', () => { + it('should overwrite investigation_fields value on update - non additive', async () => { + await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + investigation_fields: ['blob', 'boop'], + }); + + const rulePatch = { + rule_id: 'rule-1', + investigation_fields: ['foo', 'bar'], + }; + + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rulePatch) + .expect(200); + + expect(body.investigation_fields).to.eql(['foo', 'bar']); + }); + }); }); describe('patch per-action frequencies', () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 7b2dfca1f46fd..0f26e2b396db1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -841,6 +841,28 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + describe('investigation_fields', () => { + it('should overwrite investigation_fields value on update - non additive', async () => { + await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + investigation_fields: ['blob', 'boop'], + }); + + const ruleUpdate = { + ...getSimpleRuleUpdate('rule-1'), + investigation_fields: ['foo', 'bar'], + }; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate) + .expect(200); + + expect(body.investigation_fields).to.eql(['foo', 'bar']); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts index 9e7b6265a2b9f..1435565286485 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts @@ -337,6 +337,7 @@ export default ({ getService }: FtrProviderContext) => { max_signals: 100, risk_score_mapping: [], severity_mapping: [], + investigation_fields: [], threat: [], to: 'now', references: [], @@ -512,6 +513,7 @@ export default ({ getService }: FtrProviderContext) => { related_integrations: [], required_fields: [], setup: '', + investigation_fields: [], }, 'kibana.alert.rule.actions': [], 'kibana.alert.rule.created_by': 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts index 792fcb30b6645..b9f6b8caed951 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts @@ -146,6 +146,7 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'machine_learning', version: 1, + investigation_fields: [], }, [ALERT_DEPTH]: 1, [ALERT_REASON]: `event with process store, by root on mothra created critical alert Test ML rule.`, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts index 0ac86c991015d..72a71185065d9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts @@ -198,6 +198,7 @@ export default ({ getService }: FtrProviderContext) => { history_window_start: '2019-01-19T20:42:00.000Z', index: ['auditbeat-*'], language: 'kuery', + investigation_fields: [], }, 'kibana.alert.rule.actions': [], 'kibana.alert.rule.author': [], diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index 0115b00c4b46b..9c35d6652935f 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -102,4 +102,5 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial = related_integrations: [], required_fields: [], setup: '', + investigation_fields: [], }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 7c51faf2b8846..92f427876e351 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -60,6 +60,7 @@ export const getMockSharedResponseSchema = ( timestamp_override: undefined, timestamp_override_fallback_disabled: undefined, namespace: undefined, + investigation_fields: [], }); const getQueryRuleOutput = (ruleId = 'rule-1', enabled = false): RuleResponse => ({ diff --git a/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts index 12b8790d38324..4450224d0456c 100644 --- a/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts @@ -147,7 +147,7 @@ export default function ({ }); }); - describe('PNG Layout', () => { + describe('Preserve Layout', () => { before(async () => { await loadEcommerce(); }); @@ -155,65 +155,6 @@ export default function ({ await unloadEcommerce(); }); - // Failing: See https://github.com/elastic/kibana/issues/142484 - it.skip('PNG file matches the baseline: large dashboard', async function () { - this.timeout(300000); - - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Large Dashboard'); - await PageObjects.reporting.openPngReportingPanel(); - await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); - await PageObjects.reporting.clickGenerateReportButton(); - await PageObjects.reporting.removeForceSharedItemsContainerSize(); - - const url = await PageObjects.reporting.getReportURL(200000); - const reportData = await PageObjects.reporting.getRawPdfReportData(url); - const reportFileName = 'large_dashboard_preserve_layout'; - const sessionReportPath = await PageObjects.reporting.writeSessionReport( - reportFileName, - 'png', - reportData, - REPORTS_FOLDER - ); - const baselinePath = PageObjects.reporting.getBaselineReportPath( - reportFileName, - 'png', - REPORTS_FOLDER - ); - const percentDiff = await png.compareAgainstBaseline( - sessionReportPath, - baselinePath, - REPORTS_FOLDER, - updateBaselines - ); - - expect(percentDiff).to.be.lessThan(0.03); - }); - }); - - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/157023 - describe.skip('Preserve Layout', () => { - before(async () => { - await loadEcommerce(); - }); - after(async () => { - await unloadEcommerce(); - }); - - it('downloads a PDF file: large dashboard', async function () { - this.timeout(300000); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Large Dashboard'); - await PageObjects.reporting.openPdfReportingPanel(); - await PageObjects.reporting.clickGenerateReportButton(); - - const url = await PageObjects.reporting.getReportURL(60000); - const res = await PageObjects.reporting.getResponse(url); - - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('application/pdf'); - }); - it('downloads a PDF file with saved search given EuiDataGrid enabled', async function () { await kibanaServer.uiSettings.update({ 'doc_table:legacy': false }); this.timeout(300000); diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 79fa4d0eea06e..7fedb18db416c 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -199,8 +199,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/157711 - describe.skip('alerts flyouts', () => { + describe('alerts flyouts', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await pageObjects.common.navigateToApp('infraOps'); @@ -217,7 +216,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.infraHome.closeAlertFlyout(); }); - it('should open and close inventory alert flyout', async () => { + it('should open and close metrics threshold alert flyout', async () => { await pageObjects.infraHome.openMetricsThresholdAlertFlyout(); await pageObjects.infraHome.closeAlertFlyout(); }); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 527124c34ef74..afe19a1ae1938 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -17,8 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/89072 - describe.skip('overview page', function () { + describe('overview page', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; @@ -199,7 +198,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can change query syntax to kql', async () => { await testSubjects.click('switchQueryLanguageButton'); - await testSubjects.click('languageToggle'); + await testSubjects.click('kqlLanguageMenuItem'); }); it('runs filter query without issues', async () => { diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index f9f088b106375..aed3558cfdcb4 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -335,20 +335,40 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.missingOrFail('metrics-alert-menu'); }, + async dismissDatePickerTooltip() { + const isTooltipOpen = await testSubjects.exists(`waffleDatePickerIntervalTooltip`, { + timeout: 1000, + }); + + if (isTooltipOpen) { + await testSubjects.click(`waffleDatePickerIntervalTooltip`); + } + }, + async openInventoryAlertFlyout() { + await this.dismissDatePickerTooltip(); await testSubjects.click('infrastructure-alerts-and-rules'); await testSubjects.click('inventory-alerts-menu-option'); - await testSubjects.click('inventory-alerts-create-rule'); + + // forces date picker tooltip to close in case it pops up after Alerts and rules opens + await testSubjects.moveMouseTo('contextMenuPanelTitleButton'); + + await retry.tryForTime(1000, () => testSubjects.click('inventory-alerts-create-rule')); await testSubjects.missingOrFail('inventory-alerts-create-rule', { timeout: 30000 }); - await testSubjects.find('euiFlyoutCloseButton'); }, async openMetricsThresholdAlertFlyout() { + await this.dismissDatePickerTooltip(); await testSubjects.click('infrastructure-alerts-and-rules'); await testSubjects.click('metrics-threshold-alerts-menu-option'); - await testSubjects.click('metrics-threshold-alerts-create-rule'); + + // forces date picker tooltip to close in case it pops up after Alerts and rules opens + await testSubjects.moveMouseTo('contextMenuPanelTitleButton'); + + await retry.tryForTime(1000, () => + testSubjects.click('metrics-threshold-alerts-create-rule') + ); await testSubjects.missingOrFail('metrics-threshold-alerts-create-rule', { timeout: 30000 }); - await testSubjects.find('euiFlyoutCloseButton'); }, async closeAlertFlyout() { diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index 4488c340a2b6b..d78aea22f8167 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -45,7 +45,7 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider await this.setKueryBarText('queryInput', filterQuery); }, async goToNextPage() { - await testSubjects.click('xpack.synthetics.monitorList.nextButton', 5000); + await testSubjects.click('xpack.uptime.monitorList.nextButton', 5000); }, async goToPreviousPage() { await testSubjects.click('xpack.synthetics.monitorList.prevButton', 5000); @@ -97,11 +97,11 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider }; }, async openPageSizeSelectPopover(): Promise { - return testSubjects.click('xpack.synthetics.monitorList.pageSizeSelect.popoverOpen', 5000); + return testSubjects.click('xpack.uptime.monitorList.pageSizeSelect.popoverOpen', 5000); }, async clickPageSizeSelectPopoverItem(size: number = 10): Promise { return testSubjects.click( - `xpack.synthetics.monitorList.pageSizeSelect.sizeSelectItem${size.toString()}`, + `xpack.uptime.monitorList.pageSizeSelect.sizeSelectItem${size.toString()}`, 5000 ); }, diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts index 7eec8e96ef380..d3230de9d0b10 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts @@ -99,6 +99,39 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { ); }); + it('trims fields correctly while creating a case', async () => { + const titleWithSpace = 'This is a title with spaces '; + const descriptionWithSpace = + 'This is a case description with empty spaces at the end!! '; + const categoryWithSpace = 'security '; + const tagWithSpace = 'coke '; + + await cases.create.openCreateCasePage(); + await cases.create.createCase({ + title: titleWithSpace, + description: descriptionWithSpace, + tag: tagWithSpace, + severity: CaseSeverity.HIGH, + category: categoryWithSpace, + }); + + // validate title is trimmed + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); + expect(await title.getVisibleText()).equal(titleWithSpace.trim()); + + // validate description is trimmed + const description = await testSubjects.find('scrollable-markdown'); + expect(await description.getVisibleText()).equal(descriptionWithSpace.trim()); + + // validate tag exists and is trimmed + const tag = await testSubjects.find(`tag-${tagWithSpace.trim()}`); + expect(await tag.getVisibleText()).equal(tagWithSpace.trim()); + + // validate category exists and is trimmed + const category = await testSubjects.find(`category-viewer-${categoryWithSpace.trim()}`); + expect(await category.getVisibleText()).equal(categoryWithSpace.trim()); + }); + describe('Assignees', function () { before(async () => { await createUsersAndRoles(getService, users, roles); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts index 493a9e0764a8f..8a5b935df34ee 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/view_case.ts @@ -87,6 +87,36 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('editable-title-cancel-btn'); }); + it('shows error when description is empty strings, trims the description value on submit', async () => { + await testSubjects.click('description-edit-icon'); + + await header.waitUntilLoadingHasFinished(); + + const editCommentTextArea = await find.byCssSelector( + '[data-test-subj*="editable-markdown-form"] textarea.euiMarkdownEditorTextArea' + ); + + await header.waitUntilLoadingHasFinished(); + + await editCommentTextArea.focus(); + await editCommentTextArea.clearValue(); + await editCommentTextArea.type(' '); + + const error = await find.byCssSelector('.euiFormErrorText'); + expect(await error.getVisibleText()).equal('A description is required.'); + + await editCommentTextArea.type('Description with space '); + + await testSubjects.click('editable-save-markdown'); + await header.waitUntilLoadingHasFinished(); + + const desc = await find.byCssSelector( + '[data-test-subj="description"] [data-test-subj="scrollable-markdown"]' + ); + + expect(await desc.getVisibleText()).equal('Description with space'); + }); + it('adds a comment to a case', async () => { const commentArea = await find.byCssSelector( '[data-test-subj="add-comment"] textarea.euiMarkdownEditorTextArea' diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts index 59aa3a5abe793..1a1b47e925c91 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts @@ -15,7 +15,11 @@ import { PAGE_TITLE } from '../../screens/common/page'; import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../../tasks/login'; import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { createRule, deleteCustomRule } from '../../tasks/api_calls/rules'; -import { getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; +import { + getCallOut, + NEED_ADMIN_FOR_UPDATE_CALLOUT, + waitForCallOutToBeShown, +} from '../../tasks/common/callouts'; const loadPageAsPlatformEngineerUser = (url: string) => { login(ROLES.soc_manager); @@ -31,8 +35,6 @@ describe( 'Detections > Need Admin Callouts indicating an admin is needed to migrate the alert data set', { tags: tag.ESS }, () => { - const NEED_ADMIN_FOR_UPDATE_CALLOUT = 'need-admin-for-update-rules'; - before(() => { // First, we have to open the app on behalf of a privileged user in order to initialize it. // Otherwise the app will be disabled and show a "welcome"-like page. diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts index 4ca60eebad297..767b2ecbdd5c2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts @@ -15,7 +15,12 @@ import { PAGE_TITLE } from '../../screens/common/page'; import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../../tasks/login'; import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { createRule, deleteCustomRule } from '../../tasks/api_calls/rules'; -import { getCallOut, waitForCallOutToBeShown, dismissCallOut } from '../../tasks/common/callouts'; +import { + getCallOut, + waitForCallOutToBeShown, + dismissCallOut, + MISSING_PRIVILEGES_CALLOUT, +} from '../../tasks/common/callouts'; const loadPageAsReadOnlyUser = (url: string) => { login(ROLES.reader); @@ -39,8 +44,6 @@ const waitForPageTitleToBeShown = () => { }; describe('Detections > Callouts', { tags: tag.ESS }, () => { - const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; - before(() => { // First, we have to open the app on behalf of a privileged user in order to initialize it. // Otherwise the app will be disabled and show a "welcome"-like page. diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts index 62f484b69427a..bd8c5743d37b2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts @@ -22,12 +22,11 @@ import { dismissCallOut, getCallOut, waitForCallOutToBeShown, + MISSING_PRIVILEGES_CALLOUT, } from '../../../../tasks/common/callouts'; import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { SECURITY_DETECTIONS_RULES_URL } from '../../../../urls/navigation'; -const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; - describe('All rules - read only', { tags: tag.ESS }, () => { before(() => { cleanKibana(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts index d6000c671fb86..3b3755dc66d3f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts @@ -7,6 +7,11 @@ import type { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; +import { + MISSING_PRIVILEGES_CALLOUT, + waitForCallOutToBeShown, +} from '../../../../../tasks/common/callouts'; +import { createRuleAssetSavedObject } from '../../../../../helpers/rules'; import { tag } from '../../../../../tags'; import { @@ -34,6 +39,7 @@ import { waitForRulesTableToBeLoaded, selectNumberOfRules, goToEditRuleActionsSettingsOf, + disableAutoRefresh, } from '../../../../../tasks/alerts_detection_rules'; import { waitForBulkEditActionToFinish, @@ -57,28 +63,28 @@ import { getMachineLearningRule, getNewTermsRule, } from '../../../../../objects/rule'; -import { excessivelyInstallAllPrebuiltRules } from '../../../../../tasks/api_calls/prebuilt_rules'; +import { + createAndInstallMockedPrebuiltRules, + excessivelyInstallAllPrebuiltRules, + preventPrebuiltRulesPackageInstallation, +} from '../../../../../tasks/api_calls/prebuilt_rules'; const ruleNameToAssert = 'Custom rule name with actions'; const expectedNumberOfCustomRulesToBeEdited = 7; -// 7 custom rules of different types + 3 prebuilt. +// 7 custom rules of different types + 2 prebuilt. // number of selected rules doesn't matter, we only want to make sure they will be edited an no modal window displayed as for other actions -const expectedNumberOfRulesToBeEdited = expectedNumberOfCustomRulesToBeEdited + 3; +const expectedNumberOfRulesToBeEdited = expectedNumberOfCustomRulesToBeEdited + 2; const expectedExistingSlackMessage = 'Existing slack action'; const expectedSlackMessage = 'Slack action test message'; -// TODO: Fix flakiness and unskip https://github.com/elastic/kibana/issues/154721 -describe.skip( +describe( 'Detection rules, bulk edit of rule actions', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { - before(() => { + beforeEach(() => { cleanKibana(); login(); - }); - - beforeEach(() => { deleteAlertsAndRules(); deleteConnectors(); cy.task('esArchiverResetKibana'); @@ -111,12 +117,27 @@ describe.skip( createRule(getNewRule({ saved_id: 'mocked', rule_id: '7' })); createSlackConnector(); + + // Prevent prebuilt rules package installation and mock two prebuilt rules + preventPrebuiltRulesPackageInstallation(); + + const RULE_1 = createRuleAssetSavedObject({ + name: 'Test rule 1', + rule_id: 'rule_1', + }); + const RULE_2 = createRuleAssetSavedObject({ + name: 'Test rule 2', + rule_id: 'rule_2', + }); + + createAndInstallMockedPrebuiltRules({ rules: [RULE_1, RULE_2] }); }); context('Restricted action privileges', () => { it("User with no privileges can't add rule actions", () => { login(ROLES.hunter_no_actions); visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL, ROLES.hunter_no_actions); + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); waitForRulesTableToBeLoaded(); selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); @@ -129,8 +150,10 @@ describe.skip( context('All actions privileges', () => { beforeEach(() => { + login(); visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); waitForRulesTableToBeLoaded(); + disableAutoRefresh(); }); it('Add a rule action to rules (existing connector)', () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts index 824699e2cd230..9ac8cfbe5ddd0 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts @@ -58,7 +58,7 @@ const expectedNumberOfCustomRulesToBeEdited = 6; describe( 'Bulk editing index patterns of rules with a data view only', - { tags: [tag.ESS, tag.SERVERLESS] }, + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { before(() => { cleanKibana(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts index 3e29f3a08cb70..bef92ab69444b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts @@ -42,7 +42,8 @@ import { } from '../../../screens/exceptions'; import { goToEndpointExceptionsTab } from '../../../tasks/rule_details'; -describe( +// See https://github.com/elastic/kibana/issues/163967 +describe.skip( 'Endpoint Exceptions workflows from Alert', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts index 6a7df890aec06..5a2451d42d86e 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts @@ -39,7 +39,8 @@ import { } from '../../../../screens/exceptions'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( +// See https://github.com/elastic/kibana/issues/163967 +describe.skip( 'Auto populate exception with Alert data', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts index 96bf48fa27935..ea905a7774126 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts @@ -4,13 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; import { addExceptionFromFirstAlert, goToClosedAlertsOnRuleDetailsPage, waitForAlerts, } from '../../../../tasks/alerts'; import { deleteAlertsAndRules, postDataView } from '../../../../tasks/common'; -import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../../urls/navigation'; import { goToRuleDetails } from '../../../../tasks/alerts_detection_rules'; @@ -27,6 +27,7 @@ import { submitNewExceptionItem, } from '../../../../tasks/exceptions'; +// See https://github.com/elastic/kibana/issues/163967 describe('Close matching Alerts ', () => { const newRule = getNewRule(); const ITEM_NAME = 'Sample Exception Item'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts index fc91468b88c56..c86f79ee0e264 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts @@ -60,277 +60,284 @@ import { } from '../../../tasks/api_calls/exceptions'; import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; -describe('Add/edit exception from rule details', { tags: [tag.ESS, tag.SERVERLESS] }, () => { - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; - const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name'; - const ITEM_FIELD = 'unique_value.test'; - - before(() => { - cy.task('esArchiverResetKibana'); - cy.task('esArchiverLoad', 'exceptions'); - login(); - }); - - after(() => { - cy.task('esArchiverUnload', 'exceptions'); - }); - - describe('existing list and items', () => { - const exceptionList = getExceptionList(); - beforeEach(() => { - deleteAlertsAndRules(); - deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); - // create rule with exceptions - createExceptionList(exceptionList, exceptionList.list_id).then((response) => { - createRule( - getNewRule({ - query: 'agent.name:*', - index: ['exceptions*'], - exceptions_list: [ +describe( + 'Add/edit exception from rule details', + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, + () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name'; + const ITEM_FIELD = 'unique_value.test'; + + before(() => { + cy.task('esArchiverResetKibana'); + cy.task('esArchiverLoad', 'exceptions'); + login(); + }); + + after(() => { + cy.task('esArchiverUnload', 'exceptions'); + }); + + describe('existing list and items', () => { + const exceptionList = getExceptionList(); + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createRule( + getNewRule({ + query: 'agent.name:*', + index: ['exceptions*'], + exceptions_list: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + rule_id: '2', + }) + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item 2', + name: 'Sample Exception List Item 2', + namespace_type: 'single', + entries: [ { - id: response.body.id, - list_id: exceptionList.list_id, - type: exceptionList.type, - namespace_type: exceptionList.namespace_type, + field: ITEM_FIELD, + operator: 'included', + type: 'match_any', + value: ['foo'], }, ], - rule_id: '2', - }) - ); - createExceptionListItem(exceptionList.list_id, { - list_id: exceptionList.list_id, - item_id: 'simple_list_item', - tags: [], - type: 'simple', - description: 'Test exception item 2', - name: 'Sample Exception List Item 2', - namespace_type: 'single', - entries: [ - { - field: ITEM_FIELD, - operator: 'included', - type: 'match_any', - value: ['foo'], - }, - ], + }); }); - }); - login(); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - goToExceptionsTab(); - }); + login(); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); - it('Edits an exception item', () => { - const NEW_ITEM_NAME = 'Exception item-EDITED'; - const ITEM_NAME = 'Sample Exception List Item 2'; + it('Edits an exception item', () => { + const NEW_ITEM_NAME = 'Exception item-EDITED'; + const ITEM_NAME = 'Sample Exception List Item 2'; - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME); - cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should('have.text', ' unique_value.testis one of foo'); + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should( + 'have.text', + ' unique_value.testis one of foo' + ); - // open edit exception modal - openEditException(); + // open edit exception modal + openEditException(); - // edit exception item name - editExceptionFlyoutItemName(NEW_ITEM_NAME); + // edit exception item name + editExceptionFlyoutItemName(NEW_ITEM_NAME); - // check that the existing item's field is being populated - cy.get(EXCEPTION_ITEM_CONTAINER) - .eq(0) - .find(FIELD_INPUT_PARENT) - .eq(0) - .should('have.text', ITEM_FIELD); - cy.get(VALUES_MATCH_ANY_INPUT).should('have.text', 'foo'); + // check that the existing item's field is being populated + cy.get(EXCEPTION_ITEM_CONTAINER) + .eq(0) + .find(FIELD_INPUT_PARENT) + .eq(0) + .should('have.text', ITEM_FIELD); + cy.get(VALUES_MATCH_ANY_INPUT).should('have.text', 'foo'); - // edit conditions - editException(FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD, 0, 0); + // edit conditions + editException(FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD, 0, 0); - // submit - submitEditedExceptionItem(); + // submit + submitEditedExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - // check that updates stuck - cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', NEW_ITEM_NAME); - cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should('have.text', ' agent.nameIS foo'); - }); + // check that updates stuck + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', NEW_ITEM_NAME); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should('have.text', ' agent.nameIS foo'); + }); - describe('rule with existing shared exceptions', () => { - it('Creates an exception item to add to shared list', () => { - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + describe('rule with existing shared exceptions', () => { + it('Creates an exception item to add to shared list', () => { + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - // open add exception modal - addExceptionFlyoutFromViewerHeader(); + // open add exception modal + addExceptionFlyoutFromViewerHeader(); - // add exception item conditions - addExceptionConditions(getException()); + // add exception item conditions + addExceptionConditions(getException()); - // Name is required so want to check that submit is still disabled - cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + // Name is required so want to check that submit is still disabled + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); - // add exception item name - addExceptionFlyoutItemName('My item name'); + // add exception item name + addExceptionFlyoutItemName('My item name'); - // select to add exception item to a shared list - selectSharedListToAddExceptionTo(1); + // select to add exception item to a shared list + selectSharedListToAddExceptionTo(1); - // not testing close alert functionality here, just ensuring that the options appear as expected - cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); - cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); + // not testing close alert functionality here, just ensuring that the options appear as expected + cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); + cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); - // submit - submitNewExceptionItem(); + // submit + submitNewExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); - }); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + }); - it('Creates an exception item to add to rule only', () => { - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + it('Creates an exception item to add to rule only', () => { + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - // open add exception modal - addExceptionFlyoutFromViewerHeader(); + // open add exception modal + addExceptionFlyoutFromViewerHeader(); - // add exception item conditions - addExceptionConditions(getException()); + // add exception item conditions + addExceptionConditions(getException()); - // Name is required so want to check that submit is still disabled - cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + // Name is required so want to check that submit is still disabled + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); - // add exception item name - addExceptionFlyoutItemName('My item name'); + // add exception item name + addExceptionFlyoutItemName('My item name'); - // select to add exception item to rule only - selectAddToRuleRadio(); + // select to add exception item to rule only + selectAddToRuleRadio(); - // not testing close alert functionality here, just ensuring that the options appear as expected - cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); - cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); + // not testing close alert functionality here, just ensuring that the options appear as expected + cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); + cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); - // submit - submitNewExceptionItem(); + // submit + submitNewExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); - }); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + }); - // Trying to figure out with EUI why the search won't trigger - it('Can search for items', () => { - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + // Trying to figure out with EUI why the search won't trigger + it('Can search for items', () => { + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - // can search for an exception value - searchForExceptionItem('foo'); + // can search for an exception value + searchForExceptionItem('foo'); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - // displays empty search result view if no matches found - searchForExceptionItem('abc'); + // displays empty search result view if no matches found + searchForExceptionItem('abc'); - // new exception item displays - cy.get(NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT).should('exist'); + // new exception item displays + cy.get(NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT).should('exist'); + }); }); }); - }); - - describe('rule without existing exceptions', () => { - beforeEach(() => { - deleteAlertsAndRules(); - createRule( - getNewRule({ - query: 'agent.name:*', - index: ['exceptions*'], - interval: '10s', - rule_id: 'rule_testing', - }) - ); - login(); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - goToExceptionsTab(); - }); - afterEach(() => { - cy.task('esArchiverUnload', 'exceptions_2'); - }); + describe('rule without existing exceptions', () => { + beforeEach(() => { + deleteAlertsAndRules(); + createRule( + getNewRule({ + query: 'agent.name:*', + index: ['exceptions*'], + interval: '10s', + rule_id: 'rule_testing', + }) + ); + login(); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); - it('Cannot create an item to add to rule but not shared list as rule has no lists attached', () => { - // when no exceptions exist, empty component shows with action to add exception - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + afterEach(() => { + cy.task('esArchiverUnload', 'exceptions_2'); + }); - // open add exception modal - openExceptionFlyoutFromEmptyViewerPrompt(); + it('Cannot create an item to add to rule but not shared list as rule has no lists attached', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); - // add exception item conditions - addExceptionConditions({ - field: 'agent.name', - operator: 'is', - values: ['foo'], - }); + // open add exception modal + openExceptionFlyoutFromEmptyViewerPrompt(); + + // add exception item conditions + addExceptionConditions({ + field: 'agent.name', + operator: 'is', + values: ['foo'], + }); - // Name is required so want to check that submit is still disabled - cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + // Name is required so want to check that submit is still disabled + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); - // add exception item name - addExceptionFlyoutItemName('My item name'); + // add exception item name + addExceptionFlyoutItemName('My item name'); - // select to add exception item to rule only - selectAddToRuleRadio(); + // select to add exception item to rule only + selectAddToRuleRadio(); - // Check that add to shared list is disabled, should be unless - // rule has shared lists attached to it already - cy.get(ADD_TO_SHARED_LIST_RADIO_INPUT).should('have.attr', 'disabled'); + // Check that add to shared list is disabled, should be unless + // rule has shared lists attached to it already + cy.get(ADD_TO_SHARED_LIST_RADIO_INPUT).should('have.attr', 'disabled'); - // Close matching alerts - selectBulkCloseAlerts(); + // Close matching alerts + selectBulkCloseAlerts(); - // submit - submitNewExceptionItem(); + // submit + submitNewExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - // Alerts table should now be empty from having added exception and closed - // matching alert - goToAlertsTab(); - cy.get(EMPTY_ALERT_TABLE).should('exist'); + // Alerts table should now be empty from having added exception and closed + // matching alert + goToAlertsTab(); + cy.get(EMPTY_ALERT_TABLE).should('exist'); - // Closed alert should appear in table - goToClosedAlertsOnRuleDetailsPage(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(ALERTS_COUNT).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + // Closed alert should appear in table + goToClosedAlertsOnRuleDetailsPage(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(ALERTS_COUNT).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); - // when removing exception and again, no more exist, empty screen shows again - removeException(); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + // when removing exception and again, no more exist, empty screen shows again + removeException(); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); - // load more docs - cy.task('esArchiverLoad', 'exceptions_2'); + // load more docs + cy.task('esArchiverLoad', 'exceptions_2'); - // now that there are no more exceptions, the docs should match and populate alerts - goToAlertsTab(); - waitForAlertsToPopulate(); - goToOpenedAlertsOnRuleDetailsPage(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); + // now that there are no more exceptions, the docs should match and populate alerts + goToAlertsTab(); + waitForAlertsToPopulate(); + goToOpenedAlertsOnRuleDetailsPage(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(ALERTS_COUNT).should('have.text', '2 alerts'); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(ALERTS_COUNT).should('have.text', '2 alerts'); + }); }); - }); -}); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts index 1d020462683da..ab0595b23f889 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts @@ -44,7 +44,7 @@ import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; describe( 'Add exception using data views from rule details', - { tags: [tag.ESS, tag.SERVERLESS] }, + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; const ITEM_NAME = 'Sample Exception List Item'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts index 25b9d2e34fe2e..b11d3de105b83 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts @@ -17,12 +17,11 @@ import { dismissCallOut, getCallOut, waitForCallOutToBeShown, + MISSING_PRIVILEGES_CALLOUT, } from '../../../../tasks/common/callouts'; import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { EXCEPTIONS_URL } from '../../../../urls/navigation'; -const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; - describe('Shared exception lists - read only', { tags: tag.ESS }, () => { before(() => { cy.task('esArchiverResetKibana'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts index e9ea4d15b6152..bfb97b6bc9c34 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts @@ -108,7 +108,7 @@ const assertFilterControlsWithFilterObject = ( }); }; -describe(`Detections : Page Filters`, { tags: [tag.ESS, tag.SERVERLESS] }, () => { +describe(`Detections : Page Filters`, { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { before(() => { cleanKibana(); createRule(getNewRule({ rule_id: 'custom_rule_filters' })); @@ -235,7 +235,7 @@ describe(`Detections : Page Filters`, { tags: [tag.ESS, tag.SERVERLESS] }, () => cy.get(FILTER_GROUP_CHANGED_BANNER).should('be.visible'); }); - context('with data modificiation', () => { + context.skip('with data modificiation', () => { after(() => { cleanKibana(); createRule(getNewRule({ rule_id: 'custom_rule_filters' })); @@ -363,7 +363,7 @@ describe(`Detections : Page Filters`, { tags: [tag.ESS, tag.SERVERLESS] }, () => value: 'invalid', }); waitForPageFilters(); - togglePageFilterPopover(0); + openPageFilterPopover(0); cy.get(CONTROL_POPOVER(0)).should('contain.text', 'No options found'); cy.get(EMPTY_ALERT_TABLE).should('be.visible'); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_alert_reason_preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_alert_reason_preview.cy.ts new file mode 100644 index 0000000000000..83d2dbee62212 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_alert_reason_preview.cy.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 { DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER } from '../../../../screens/expandable_flyout/alert_details_preview_panel_alert_reason_preview'; +import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_flyout/common'; +import { clickAlertReasonButton } from '../../../../tasks/expandable_flyout/alert_details_right_panel_overview_tab'; +import { cleanKibana } from '../../../../tasks/common'; +import { login, visit } from '../../../../tasks/login'; +import { createRule } from '../../../../tasks/api_calls/rules'; +import { getNewRule } from '../../../../objects/rule'; +import { ALERTS_URL } from '../../../../urls/navigation'; +import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; +import { tag } from '../../../../tags'; + +describe( + 'Alert details expandable flyout rule preview panel', + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, + () => { + const rule = getNewRule(); + + beforeEach(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + clickAlertReasonButton(); + }); + + describe('alert reason preview', () => { + it('should display alert reason preview', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER).should('be.visible'); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index 2cdf95746dcfa..050463b70ae50 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -111,7 +111,8 @@ describe( cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE) .should('be.visible') - .and('have.text', 'Alert reason'); + .and('contain.text', 'Alert reason') + .and('contain.text', 'Show full reason'); cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS) .should('be.visible') .and('contain.text', rule.name); diff --git a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts index e2920f4975478..586a4188b84b1 100644 --- a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts @@ -532,6 +532,7 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response { return cy.get(callOutWithId(id), options); }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/common/filter_group.ts b/x-pack/test/security_solution_cypress/cypress/tasks/common/filter_group.ts index d3b1c5857bb45..fb57aa7329ab0 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/common/filter_group.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/common/filter_group.ts @@ -31,6 +31,7 @@ import { waitForPageFilters } from '../alerts'; export const openFilterGroupContextMenu = () => { recurse( () => { + cy.get(DETECTION_PAGE_FILTER_GROUP_CONTEXT_MENU_BTN).scrollIntoView(); cy.get(DETECTION_PAGE_FILTER_GROUP_CONTEXT_MENU_BTN).click(); return cy.get(DETECTION_PAGE_FILTER_GROUP_CONTEXT_MENU).should(Cypress._.noop); }, diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts index c87a3c2afb9fe..b94a8be090fa4 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -20,6 +20,8 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_HEADER, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON, } from '../../screens/expandable_flyout/alert_details_right_panel_overview_tab'; /* About section */ @@ -129,3 +131,17 @@ export const clickRuleSummaryButton = () => { .click(); }); }; + +/** + * Click `Show full reason` button to open alert reason preview panel + */ +export const clickAlertReasonButton = () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE) + .should('be.visible') + .within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON) + .should('be.visible') + .click(); + }); +}; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index d32b7ee11c268..3c66c28d570c0 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -114,8 +114,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.endpoint.navigateToEndpointList(); }); - // FLAKY: https://github.com/elastic/kibana/issues/163883 - describe.skip('when there is data,', () => { + describe('when there is data,', () => { before(async () => { indexedData = await endpointTestResources.loadEndpointData({ numHosts: 3 }); await pageObjects.endpoint.navigateToEndpointList(); diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts index f986c71f1d66d..f6584f9802e51 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts @@ -51,9 +51,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true'); }; - // FLAKY: https://github.com/elastic/kibana/issues/159695 - // FLAKY: https://github.com/elastic/kibana/issues/159696 - describe.skip('For each artifact list under management', function () { + describe('For each artifact list under management', function () { this.timeout(60_000 * 5); let indexedData: IndexedHostsAndAlertsResponse; let policyInfo: PolicyTestResourceInfo; diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts index 2814706109d60..0093119a21e18 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/policy_details.ts @@ -27,9 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const endpointTestResources = getService('endpointTestResources'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/163889 - // FLAKY: https://github.com/elastic/kibana/issues/163890 - describe.skip('When on the Endpoint Policy Details Page', function () { + describe('When on the Endpoint Policy Details Page', function () { let indexedData: IndexedHostsAndAlertsResponse; const formTestSubjects = getPolicySettingsFormTestSubjects(); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 3b6f9d5c278f2..4eb92d2dee08b 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -394,8 +394,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/160274 - describe.skip('get metadata transforms', () => { + describe('get metadata transforms', () => { const testRegex = /endpoint\.metadata_(united|current)-default-*/; it('should respond forbidden if no fleet access', async () => { diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts index 86fdf7afb842a..c263a6f540c3e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts @@ -34,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); - describe('Alerting rules', () => { + describe.skip('Alerting rules', () => { const RULE_TYPE_ID = '.es-query'; const ALERT_ACTION_INDEX = 'alert-action-es-query'; let actionId: string; diff --git a/yarn.lock b/yarn.lock index 42e3b4e2a799f..0569bafd6f6ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1549,12 +1549,12 @@ "@elastic/transport" "^8.3.1" tslib "^2.4.0" -"@elastic/elasticsearch@npm:@elastic/elasticsearch@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.9.0.tgz#d132021c6c12e4171fe14371609a5c69b535edd4" - integrity sha512-UyolnzjOYTRL2966TYS3IoJP4tQbvak/pmYmbP3JdphD53RjkyVDdxMpTBv+2LcNBRrvYPTzxQbpRW/nGSXA9g== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@8.9.1-canary.1": + version "8.9.1-canary.1" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.9.1-canary.1.tgz#7c1cdc6cc4129910544b2a3abd6a73b9fcc82ff3" + integrity sha512-pxFP57AEmbsgC6LsGv7xyAR4qCXiX6JXAGVdzBXDl2qEdz1p5y3htgyT6tGvyTV11Ma0AflsWx0jJ1vrp6bGew== dependencies: - "@elastic/transport" "^8.3.2" + "@elastic/transport" "^8.3.3" tslib "^2.4.0" "@elastic/ems-client@8.4.0": @@ -1734,22 +1734,10 @@ undici "^5.21.2" yaml "^2.2.2" -"@elastic/transport@^8.3.1": - version "8.3.1" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.1.tgz#e7569d7df35b03108ea7aa886113800245faa17f" - integrity sha512-jv/Yp2VLvv5tSMEOF8iGrtL2YsYHbpf4s+nDsItxUTLFTzuJGpnsB/xBlfsoT2kAYEnWHiSJuqrbRcpXEI/SEQ== - dependencies: - debug "^4.3.4" - hpagent "^1.0.0" - ms "^2.1.3" - secure-json-parse "^2.4.0" - tslib "^2.4.0" - undici "^5.5.1" - -"@elastic/transport@^8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.2.tgz#295e91f43e3a60a839f998ac3090a83ddb441592" - integrity sha512-ZiBYRVPj6pwYW99fueyNU4notDf7ZPs7Ix+4T1btIJsKJmeaORIItIfs+0O7KV4vV+DcvyMhkY1FXQx7kQOODw== +"@elastic/transport@^8.3.1", "@elastic/transport@^8.3.3": + version "8.3.3" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.3.tgz#06c5b1b9566796775ac96d17959dafc269da5ec1" + integrity sha512-g5nc//dq/RQUTMkJUB8Ui8KJa/WflWmUa7yLl4SRZd67PPxIp3cn+OvGMNIhpiLRcfz1upanzgZHb/7Po2eEdQ== dependencies: debug "^4.3.4" hpagent "^1.0.0" @@ -4748,6 +4736,10 @@ version "0.0.0" uid "" +"@kbn/lens-embeddable-utils@link:packages/kbn-lens-embeddable-utils": + version "0.0.0" + uid "" + "@kbn/lens-plugin@link:x-pack/plugins/lens": version "0.0.0" uid "" @@ -6020,10 +6012,6 @@ version "0.0.0" uid "" -"@kbn/lens-embeddable-utils@link:packages/kbn-lens-embeddable-utils": - version "0.0.0" - uid "" - "@kbn/visualizations-plugin@link:src/plugins/visualizations": version "0.0.0" uid "" @@ -9739,7 +9727,7 @@ "@types/cookiejar" "*" "@types/node" "*" -"@types/supertest@^2.0.5": +"@types/supertest@^2.0.12": version "2.0.12" resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== @@ -13025,11 +13013,16 @@ compare-versions@^6.0.0: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== -component-emitter@^1.2.0, component-emitter@^1.2.1: +component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + compress-commons@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" @@ -13206,7 +13199,7 @@ cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookiejar@^2.1.0: +cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== @@ -14609,6 +14602,14 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + dfa@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" @@ -16341,6 +16342,11 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-shallow-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" @@ -16800,15 +16806,6 @@ fork-ts-checker-webpack-plugin@^6.0.4: semver "^7.3.2" tapable "^1.0.0" -form-data@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.0.tgz#094ec359dc4b55e7d62e0db4acd76e89fe874d37" - integrity sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -16848,10 +16845,15 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" -formidable@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" - integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" formik@^2.2.9: version "2.2.9" @@ -17956,6 +17958,11 @@ heap@^0.2.6: resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw= +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + highlight.js@^10.1.1, highlight.js@~10.4.0: version "10.4.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" @@ -21627,7 +21634,7 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -21742,11 +21749,16 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, dependencies: mime-db "1.51.0" -mime@1.6.0, mime@^1.4.1: +mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mime@^2.4.4: version "2.5.2" resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" @@ -24634,13 +24646,20 @@ qs@6.9.7: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== -qs@^6.10.0, qs@^6.5.1, qs@^6.7.0: +qs@^6.10.0, qs@^6.7.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" +qs@^6.11.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -27920,21 +27939,21 @@ success-symbol@^0.1.0: resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= -superagent@^3.8.2, superagent@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" - integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== +superagent@^8.0.5, superagent@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.2.0" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.3.5" + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" supercluster@^8.0.1: version "8.0.1" @@ -27950,13 +27969,13 @@ superjson@^1.10.0: dependencies: copy-anything "^3.0.2" -supertest@^3.1.0: - version "3.4.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.4.2.tgz#bad7de2e43d60d27c8caeb8ab34a67c8a5f71aad" - integrity sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA== +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== dependencies: methods "^1.1.2" - superagent "^3.8.3" + superagent "^8.0.5" supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" @@ -28933,13 +28952,6 @@ undici@^5.21.2, undici@^5.22.1: dependencies: busboy "^1.6.0" -undici@^5.5.1: - version "5.20.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263" - integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g== - dependencies: - busboy "^1.6.0" - unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"