diff --git a/.buildkite/pull_requests.json b/.buildkite/pull_requests.json index d54f637b8f6d1..e0e7454127733 100644 --- a/.buildkite/pull_requests.json +++ b/.buildkite/pull_requests.json @@ -16,7 +16,25 @@ "trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))", "always_trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))", "skip_ci_labels": ["skip-ci", "jenkins-ci"], - "skip_target_branches": ["6.8", "7.11", "7.12"] + "skip_target_branches": ["6.8", "7.11", "7.12"], + "skip_ci_on_only_changed": [ + "^docs/", + "^rfcs/", + "^.ci/.+\\.yml$", + "^.ci/es-snapshots/", + "^.ci/pipeline-library/", + "^.ci/Jenkinsfile_[^/]+$", + "^\\.github/", + "\\.md$", + "^\\.backportrc\\.json$", + "^nav-kibana-dev\\.docnav\\.json$", + "^src/dev/prs/kibana_qa_pr_list\\.json$", + "^\\.buildkite/pull_requests\\.json$" + ], + "always_require_ci_on_changed": [ + "^docs/developer/plugin-list.asciidoc$", + "/plugins/[^/]+/readme\\.(md|asciidoc)$" + ] } ] } diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 6a4610284e400..c9f42dae1a776 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -9,14 +9,11 @@ const execSync = require('child_process').execSync; const fs = require('fs'); const { areChangesSkippable, doAnyChangesMatch } = require('kibana-buildkite-library'); -const { SKIPPABLE_PR_MATCHERS } = require('./skippable_pr_matchers'); - -const REQUIRED_PATHS = [ - // this file is auto-generated and changes to it need to be validated with CI - /^docs\/developer\/plugin-list.asciidoc$/, - // don't skip CI on prs with changes to plugin readme files /i is for case-insensitive matching - /\/plugins\/[^\/]+\/readme\.(md|asciidoc)$/i, -]; +const prConfigs = require('../../../pull_requests.json'); +const prConfig = prConfigs.jobs.find((job) => job.pipelineSlug === 'kibana-pull-request'); + +const REQUIRED_PATHS = prConfig.always_require_ci_on_changed.map((r) => new RegExp(r, 'i')); +const SKIPPABLE_PR_MATCHERS = prConfig.skip_ci_on_only_changed.map((r) => new RegExp(r, 'i')); const getPipeline = (filename, removeSteps = true) => { const str = fs.readFileSync(filename).toString(); diff --git a/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js b/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js deleted file mode 100644 index 2a36e66e11cd6..0000000000000 --- a/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js +++ /dev/null @@ -1,24 +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. - */ - -module.exports = { - SKIPPABLE_PR_MATCHERS: [ - /^docs\//, - /^rfcs\//, - /^.ci\/.+\.yml$/, - /^.ci\/es-snapshots\//, - /^.ci\/pipeline-library\//, - /^.ci\/Jenkinsfile_[^\/]+$/, - /^\.github\//, - /\.md$/, - /^\.backportrc\.json$/, - /^nav-kibana-dev\.docnav\.json$/, - /^src\/dev\/prs\/kibana_qa_pr_list\.json$/, - /^\.buildkite\/scripts\/pipelines\/pull_request\/skippable_pr_matchers\.js$/, - ], -}; diff --git a/.buildkite/scripts/steps/es_snapshots/build.sh b/.buildkite/scripts/steps/es_snapshots/build.sh index cdc1750e59bfc..370ae275aa758 100755 --- a/.buildkite/scripts/steps/es_snapshots/build.sh +++ b/.buildkite/scripts/steps/es_snapshots/build.sh @@ -69,7 +69,6 @@ echo "--- Build Elasticsearch" :distribution:archives:darwin-aarch64-tar:assemble \ :distribution:archives:darwin-tar:assemble \ :distribution:docker:docker-export:assemble \ - :distribution:docker:cloud-docker-export:assemble \ :distribution:archives:linux-aarch64-tar:assemble \ :distribution:archives:linux-tar:assemble \ :distribution:archives:windows-zip:assemble \ @@ -86,19 +85,26 @@ docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}} docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 bash -c 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' echo "--- Create kibana-ci docker cloud image archives" -ES_CLOUD_ID=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.ID}}") -ES_CLOUD_VERSION=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.Tag}}") -KIBANA_ES_CLOUD_VERSION="$ES_CLOUD_VERSION-$ELASTICSEARCH_GIT_COMMIT" -KIBANA_ES_CLOUD_IMAGE="docker.elastic.co/kibana-ci/elasticsearch-cloud:$KIBANA_ES_CLOUD_VERSION" - -docker tag "$ES_CLOUD_ID" "$KIBANA_ES_CLOUD_IMAGE" - -echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co -trap 'docker logout docker.elastic.co' EXIT -docker image push "$KIBANA_ES_CLOUD_IMAGE" - -export ELASTICSEARCH_CLOUD_IMAGE="$KIBANA_ES_CLOUD_IMAGE" -export ELASTICSEARCH_CLOUD_IMAGE_CHECKSUM="$(docker images "$KIBANA_ES_CLOUD_IMAGE" --format "{{.Digest}}")" +# Ignore build failures. This docker image downloads metricbeat and filebeat. +# When we bump versions, these dependencies may not exist yet, but we don't want to +# block the rest of the snapshot promotion process +set +e +./gradlew :distribution:docker:cloud-docker-export:assemble && { + ES_CLOUD_ID=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.ID}}") + ES_CLOUD_VERSION=$(docker images "docker.elastic.co/elasticsearch-ci/elasticsearch-cloud" --format "{{.Tag}}") + KIBANA_ES_CLOUD_VERSION="$ES_CLOUD_VERSION-$ELASTICSEARCH_GIT_COMMIT" + KIBANA_ES_CLOUD_IMAGE="docker.elastic.co/kibana-ci/elasticsearch-cloud:$KIBANA_ES_CLOUD_VERSION" + echo $ES_CLOUD_ID $ES_CLOUD_VERSION $KIBANA_ES_CLOUD_VERSION $KIBANA_ES_CLOUD_IMAGE + docker tag "$ES_CLOUD_ID" "$KIBANA_ES_CLOUD_IMAGE" + + echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co + trap 'docker logout docker.elastic.co' EXIT + docker image push "$KIBANA_ES_CLOUD_IMAGE" + + export ELASTICSEARCH_CLOUD_IMAGE="$KIBANA_ES_CLOUD_IMAGE" + export ELASTICSEARCH_CLOUD_IMAGE_CHECKSUM="$(docker images "$KIBANA_ES_CLOUD_IMAGE" --format "{{.Digest}}")" +} +set -e echo "--- Create checksums for snapshot files" cd "$destination" diff --git a/dev_docs/getting_started/troubleshooting.mdx b/dev_docs/getting_started/troubleshooting.mdx index e0adfbad86a84..db52830bbae4f 100644 --- a/dev_docs/getting_started/troubleshooting.mdx +++ b/dev_docs/getting_started/troubleshooting.mdx @@ -26,3 +26,17 @@ git clean -fdxn -e /config -e /.vscode # review the files which will be deleted, consider adding some more excludes (-e) # re-run without the dry-run (-n) flag to actually delete the files ``` + +### search.check_ccs_compatibility error + +If you run into an error that says something like: + +``` +[class org.elasticsearch.action.search.SearchRequest] is not compatible version 8.1.0 and the 'search.check_ccs_compatibility' setting is enabled. +``` + +it means you are using a new Elasticsearch feature that will not work in a CCS environment because the feature does not exist in older versions. If you are working on an experimental feature and are okay with this limitation, you will have to move the failing test into a special test suite that does not use this setting to get ci to pass. Take this path cautiously. If you do not remember to move the test back into the default test suite when the feature is GA'ed, it will not have proper CCS test coverage. + +We added this test coverage in version `8.1` because we accidentally broke core Kibana features (for example, when Discover started using the new fields parameter) for our CCS users. CCS is not a corner case and (excluding certain experimental features) Kibana should always work for our CCS users. This setting is our way of ensuring test coverage. + +Please reach out to the [Kibana Operations team](https://github.com/orgs/elastic/teams/kibana-operations) if you have further questions. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 787efa64f0775..6f7ada651ad3a 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -112,34 +112,20 @@ In addition to <.credentials {ess-icon}:: -Credentials that {kib} should use internally to authenticate anonymous requests to {es}. Possible values are: username and password, API key, or the constant `elasticsearch_anonymous_user` if you want to leverage {ref}/anonymous-access.html[{es} anonymous access]. +Credentials that {kib} should use internally to authenticate anonymous requests to {es}. + For example: + [source,yaml] ---------------------------------------- -# Username and password credentials xpack.security.authc.providers.anonymous.anonymous1: credentials: username: "anonymous_service_account" password: "anonymous_service_account_password" - -# API key (concatenated and base64-encoded) -xpack.security.authc.providers.anonymous.anonymous1: - credentials: - apiKey: "VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==" - -# API key (as returned from Elasticsearch API) -xpack.security.authc.providers.anonymous.anonymous1: - credentials: - apiKey.id: "VuaCfGcBCdbkQm-e5aOx" - apiKey.key: "ui2lp2axTNmsyakw9tvNnw" - -# Elasticsearch anonymous access -xpack.security.authc.providers.anonymous.anonymous1: - credentials: "elasticsearch_anonymous_user" ---------------------------------------- +For more information, refer to <>. + [float] [[http-authentication-settings]] ==== HTTP authentication settings diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 446de62326f8e..007d1af017df3 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -332,13 +332,11 @@ Anyone with access to the network {kib} is exposed to will be able to access {ki Anonymous authentication gives users access to {kib} without requiring them to provide credentials. This can be useful if you want your users to skip the login step when you embed dashboards in another application or set up a demo {kib} instance in your internal network, while still keeping other security features intact. -To enable anonymous authentication in {kib}, you must decide what credentials the anonymous service account {kib} should use internally to authenticate anonymous requests. +To enable anonymous authentication in {kib}, you must specify the credentials the anonymous service account {kib} should use internally to authenticate anonymous requests. NOTE: You can configure only one anonymous authentication provider per {kib} instance. -There are three ways to specify these credentials: - -If you have a user who can authenticate to {es} using username and password, for instance from the Native or LDAP security realms, you can also use these credentials to impersonate the anonymous users. Here is how your `kibana.yml` might look if you use username and password credentials: +You must have a user account that can authenticate to {es} using a username and password, for instance from the Native or LDAP security realms, so that you can use these credentials to impersonate the anonymous users. Here is how your `kibana.yml` might look: [source,yaml] ----------------------------------------------- @@ -350,45 +348,6 @@ xpack.security.authc.providers: password: "anonymous_service_account_password" ----------------------------------------------- -If using username and password credentials isn't desired or feasible, then you can create a dedicated <> for the anonymous service account. In this case, your `kibana.yml` might look like this: - -[source,yaml] ------------------------------------------------ -xpack.security.authc.providers: - anonymous.anonymous1: - order: 0 - credentials: - apiKey: "VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==" ------------------------------------------------ - -The previous configuration snippet uses an API key string that is the result of base64-encoding of the `id` and `api_key` fields returned from the {es} API, joined by a colon. You can also specify these fields separately, and {kib} will do the concatenation and base64-encoding for you: - -[source,yaml] ------------------------------------------------ -xpack.security.authc.providers: - anonymous.anonymous1: - order: 0 - credentials: - apiKey.id: "VuaCfGcBCdbkQm-e5aOx" - apiKey.key: "ui2lp2axTNmsyakw9tvNnw" ------------------------------------------------ - -It's also possible to use {kib} anonymous access in conjunction with the {es} anonymous access. - -Prior to configuring {kib}, ensure that anonymous access is enabled and properly configured in {es}. See {ref}/anonymous-access.html[Enabling anonymous access] for more information. - -Here is how your `kibana.yml` might look like if you want to use {es} anonymous access to impersonate anonymous users in {kib}: - -[source,yaml] ------------------------------------------------ -xpack.security.authc.providers: - anonymous.anonymous1: - order: 0 - credentials: "elasticsearch_anonymous_user" <1> ------------------------------------------------ - -<1> The `elasticsearch_anonymous_user` is a special constant that indicates you want to use the {es} anonymous user. - [float] ===== Anonymous access and other types of authentication diff --git a/package.json b/package.json index 30d878dca3932..69fd2ccd9e849 100644 --- a/package.json +++ b/package.json @@ -220,6 +220,7 @@ "@types/mapbox__vector-tile": "1.3.0", "@types/moment-duration-format": "^2.2.3", "@types/react-is": "^16.7.1", + "@types/rrule": "^2.2.9", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "antlr4ts": "^0.5.0-alpha.3", @@ -309,6 +310,7 @@ "loader-utils": "^1.2.3", "lodash": "^4.17.21", "lru-cache": "^4.1.5", + "luxon": "^2.3.2", "lz-string": "^1.4.4", "mapbox-gl-draw-rectangle-mode": "1.0.4", "maplibre-gl": "2.1.9", @@ -405,6 +407,7 @@ "reselect": "^4.0.0", "resize-observer-polyfill": "^1.5.1", "rison-node": "1.0.2", + "rrule": "2.6.4", "rxjs": "^7.5.5", "safe-squel": "^5.12.5", "seedrandom": "^3.0.5", diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx index 552ffa555377d..f544f21c35387 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.stories.tsx @@ -39,8 +39,9 @@ type Params = Pick & DataServiceFactoryCon export const PureComponent = (params: Params) => { const { solution, logo, hasESData, hasUserDataView } = params; + const serviceParams = { hasESData, hasUserDataView, hasDataViews: false }; - const services = servicesFactory(serviceParams); + const services = servicesFactory({ ...serviceParams, hasESData, hasUserDataView }); return ( { ); }; +export const PureComponentLoadingState = () => { + const dataCheck = () => new Promise((resolve, reject) => {}); + const services = { + ...servicesFactory({ hasESData: false, hasUserDataView: false, hasDataViews: false }), + data: { + hasESData: dataCheck, + hasUserDataView: dataCheck, + hasDataView: dataCheck, + }, + }; + return ( + + + + ); +}; + PureComponent.argTypes = { solution: { control: 'text', diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx index 82fbd222b3640..4f565e55ef52c 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; +import { EuiLoadingElastic } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { SharedUxServicesProvider, mockServicesFactory } from '@kbn/shared-ux-services'; @@ -68,4 +69,28 @@ describe('Kibana No Data Page', () => { expect(component.find(NoDataViews).length).toBe(1); expect(component.find(NoDataConfigPage).length).toBe(0); }); + + test('renders loading indicator', async () => { + const dataCheck = () => new Promise((resolve, reject) => {}); + const services = { + ...mockServicesFactory(), + data: { + hasESData: dataCheck, + hasUserDataView: dataCheck, + hasDataView: dataCheck, + }, + }; + const component = mountWithIntl( + + + + ); + + await act(() => new Promise(setImmediate)); + component.update(); + + expect(component.find(EuiLoadingElastic).length).toBe(1); + expect(component.find(NoDataViews).length).toBe(0); + expect(component.find(NoDataConfigPage).length).toBe(0); + }); }); diff --git a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx index 2e54d0d9f6a67..89ba915c07cfd 100644 --- a/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx +++ b/packages/kbn-shared-ux-components/src/empty_state/kibana_no_data_page.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import React, { useEffect, useState } from 'react'; +import { EuiLoadingElastic } from '@elastic/eui'; import { useData } from '@kbn/shared-ux-services'; import { NoDataConfigPage, NoDataPageProps } from '../page_template'; import { NoDataViews } from './no_data_views'; @@ -17,6 +18,7 @@ export interface Props { export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) => { const { hasESData, hasUserDataView } = useData(); + const [isLoading, setIsLoading] = useState(true); const [dataExists, setDataExists] = useState(false); const [hasUserDataViews, setHasUserDataViews] = useState(false); @@ -24,12 +26,19 @@ export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) => const checkData = async () => { setDataExists(await hasESData()); setHasUserDataViews(await hasUserDataView()); + setIsLoading(false); }; // TODO: add error handling // https://github.com/elastic/kibana/issues/130913 - checkData().catch(() => {}); + checkData().catch(() => { + setIsLoading(false); + }); }, [hasESData, hasUserDataView]); + if (isLoading) { + return ; + } + if (!dataExists) { return ; } diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 5427c61b485df..0ad4756e9177b 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -8,7 +8,7 @@ import dateMath from '@kbn/datemath'; import classNames from 'classnames'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import useObservable from 'react-use/lib/useObservable'; import type { Filter } from '@kbn/es-query'; @@ -126,6 +126,20 @@ const SharingMetaFields = React.memo(function SharingMetaFields({ export const QueryBarTopRow = React.memo( function QueryBarTopRow(props: QueryBarTopRowProps) { const isMobile = useIsWithinBreakpoints(['xs', 's']); + const [isXXLarge, setIsXXLarge] = useState(false); + + useEffect(() => { + function handleResize() { + setIsXXLarge(window.innerWidth >= 1440); + } + + window.removeEventListener('resize', handleResize); + window.addEventListener('resize', handleResize); + handleResize(); + + return () => window.removeEventListener('resize', handleResize); + }, []); + const { showQueryInput = true, showDatePicker = true, @@ -367,7 +381,7 @@ export const QueryBarTopRow = React.memo( ; export type RuleTypeParams = Record; @@ -104,12 +105,13 @@ export interface Rule { apiKey: string | null; apiKeyOwner: string | null; throttle: string | null; - notifyWhen: RuleNotifyWhenType | null; muteAll: boolean; + notifyWhen: RuleNotifyWhenType | null; mutedInstanceIds: string[]; executionStatus: RuleExecutionStatus; monitoring?: RuleMonitoring; - snoozeEndTime?: Date | null; // Remove ? when this parameter is made available in the public API + snoozeSchedule?: RuleSnooze; // Remove ? when this parameter is made available in the public API + isSnoozedUntil?: Date | null; } export type SanitizedRule = Omit, 'apiKey'>; diff --git a/x-pack/plugins/alerting/common/rule_snooze_type.ts b/x-pack/plugins/alerting/common/rule_snooze_type.ts new file mode 100644 index 0000000000000..405cbef357242 --- /dev/null +++ b/x-pack/plugins/alerting/common/rule_snooze_type.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { WeekdayStr } from 'rrule'; + +export type RuleSnooze = Array<{ + duration: number; + rRule: Partial & Pick; + // For scheduled/recurring snoozes, `id` uniquely identifies them so that they can be displayed, modified, and deleted individually + id?: string; +}>; + +// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec +export interface RRuleRecord { + dtstart: string; + tzid: string; + freq?: 0 | 1 | 2 | 3 | 4 | 5 | 6; + until?: string; + count?: number; + interval?: number; + wkst?: WeekdayStr; + byweekday?: Array; + bymonth?: number[]; + bysetpos?: number[]; + bymonthday: number; + byyearday: number[]; + byweekno: number[]; + byhour: number[]; + byminute: number[]; + bysecond: number[]; +} diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index 31528c0d50683..4c0d4a00b05de 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -27,4 +27,5 @@ export { } from './rule_execution_status'; export { getRecoveredAlerts } from './get_recovered_alerts'; export { createWrappedScopedClusterClientFactory } from './wrap_scoped_cluster_client'; +export { isRuleSnoozed, getRuleSnoozeEndTime } from './is_rule_snoozed'; export { convertRuleIdsToKueryNode } from './convert_rule_ids_to_kuery_node'; diff --git a/x-pack/plugins/alerting/server/lib/is_rule_snoozed.test.ts b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.test.ts new file mode 100644 index 0000000000000..14ad981a5e903 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.test.ts @@ -0,0 +1,319 @@ +/* + * 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 sinon from 'sinon'; +import { RRule } from 'rrule'; +import { isRuleSnoozed } from './is_rule_snoozed'; +import { RRuleRecord } from '../types'; + +const DATE_9999 = '9999-12-31T12:34:56.789Z'; +const DATE_1970 = '1970-01-01T00:00:00.000Z'; +const DATE_2019 = '2019-01-01T00:00:00.000Z'; +const DATE_2019_PLUS_6_HOURS = '2019-01-01T06:00:00.000Z'; +const DATE_2020 = '2020-01-01T00:00:00.000Z'; +const DATE_2020_MINUS_1_HOUR = '2019-12-31T23:00:00.000Z'; +const DATE_2020_MINUS_1_MONTH = '2019-12-01T00:00:00.000Z'; + +const NOW = DATE_2020; + +let fakeTimer: sinon.SinonFakeTimers; + +describe('isRuleSnoozed', () => { + beforeAll(() => { + fakeTimer = sinon.useFakeTimers(new Date(NOW)); + }); + + afterAll(() => fakeTimer.restore()); + + test('returns false when snooze has not yet started', () => { + const snoozeSchedule = [ + { + duration: 100000000, + rRule: { + dtstart: DATE_9999, + tzid: 'UTC', + count: 1, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule, muteAll: false })).toBe(false); + }); + + test('returns true when snooze has started', () => { + const snoozeSchedule = [ + { + duration: 100000000, + rRule: { + dtstart: NOW, + tzid: 'UTC', + count: 1, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule, muteAll: false })).toBe(true); + }); + + test('returns false when snooze has ended', () => { + const snoozeSchedule = [ + { + duration: 100000000, + + rRule: { + dtstart: DATE_2019, + tzid: 'UTC', + count: 1, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule, muteAll: false })).toBe(false); + }); + + test('returns true when snooze is indefinite', () => { + const snoozeSchedule = [ + { + duration: 100000000, + + rRule: { + dtstart: DATE_9999, + tzid: 'UTC', + count: 1, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule, muteAll: true })).toBe(true); + }); + + test('returns as expected for an indefinitely recurring snooze', () => { + const snoozeScheduleA = [ + { + duration: 60 * 1000, + rRule: { + dtstart: DATE_2019, + tzid: 'UTC', + freq: RRule.DAILY, + interval: 1, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true); + const snoozeScheduleB = [ + { + duration: 60 * 1000, + rRule: { + dtstart: DATE_2019_PLUS_6_HOURS, + tzid: 'UTC', + freq: RRule.DAILY, + interval: 1, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false); + const snoozeScheduleC = [ + { + duration: 60 * 1000, + rRule: { + dtstart: DATE_2020_MINUS_1_HOUR, + tzid: 'UTC', + freq: RRule.HOURLY, + interval: 1, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleC, muteAll: false })).toBe(true); + }); + + test('returns as expected for a recurring snooze with limited occurrences', () => { + const snoozeScheduleA = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.HOURLY, + interval: 1, + tzid: 'UTC', + count: 8761, + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true); + const snoozeScheduleB = [ + { + duration: 60 * 1000, + + rRule: { + freq: RRule.HOURLY, + interval: 1, + tzid: 'UTC', + count: 25, + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false); + const snoozeScheduleC = [ + { + duration: 60 * 1000, + + rRule: { + freq: RRule.YEARLY, + interval: 1, + tzid: 'UTC', + count: 60, + dtstart: DATE_1970, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleC, muteAll: false })).toBe(true); + }); + + test('returns as expected for a recurring snooze with an end date', () => { + const snoozeScheduleA = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.HOURLY, + interval: 1, + tzid: 'UTC', + until: DATE_9999, + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true); + const snoozeScheduleB = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.HOURLY, + interval: 1, + tzid: 'UTC', + until: DATE_2020_MINUS_1_HOUR, + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false); + }); + + test('returns as expected for a recurring snooze on a day of the week', () => { + const snoozeScheduleA = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.WEEKLY, + interval: 1, + tzid: 'UTC', + byweekday: ['MO', 'WE', 'FR'], // Jan 1 2020 was a Wednesday + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true); + const snoozeScheduleB = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.WEEKLY, + interval: 1, + tzid: 'UTC', + byweekday: ['TU', 'TH', 'SA', 'SU'], + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false); + const snoozeScheduleC = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.WEEKLY, + interval: 1, + tzid: 'UTC', + byweekday: ['MO', 'WE', 'FR'], + count: 12, + dtstart: DATE_2020_MINUS_1_MONTH, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleC, muteAll: false })).toBe(false); + const snoozeScheduleD = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.WEEKLY, + interval: 1, + tzid: 'UTC', + byweekday: ['MO', 'WE', 'FR'], + count: 15, + dtstart: DATE_2020_MINUS_1_MONTH, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleD, muteAll: false })).toBe(true); + }); + + test('returns as expected for a recurring snooze on an nth day of the week of a month', () => { + const snoozeScheduleA = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.MONTHLY, + interval: 1, + tzid: 'UTC', + byweekday: ['+1WE'], // Jan 1 2020 was the first Wednesday of the month + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(true); + const snoozeScheduleB = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.MONTHLY, + interval: 1, + tzid: 'UTC', + byweekday: ['+2WE'], + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(false); + }); + + test('using a timezone, returns as expected for a recurring snooze on a day of the week', () => { + const snoozeScheduleA = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.WEEKLY, + interval: 1, + byweekday: ['WE'], + tzid: 'Asia/Taipei', + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleA, muteAll: false })).toBe(false); + const snoozeScheduleB = [ + { + duration: 60 * 1000, + rRule: { + freq: RRule.WEEKLY, + interval: 1, + byweekday: ['WE'], + byhour: [0], + byminute: [0], + tzid: 'UTC', + dtstart: DATE_2019, + } as RRuleRecord, + }, + ]; + expect(isRuleSnoozed({ snoozeSchedule: snoozeScheduleB, muteAll: false })).toBe(true); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/is_rule_snoozed.ts b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.ts new file mode 100644 index 0000000000000..7ae4b99e4df75 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/is_rule_snoozed.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RRule, ByWeekday, Weekday, rrulestr } from 'rrule'; +import { SanitizedRule, RuleTypeParams } from '../../common/rule'; + +type RuleSnoozeProps = Pick, 'muteAll' | 'snoozeSchedule'>; + +export function getRuleSnoozeEndTime(rule: RuleSnoozeProps): Date | null { + if (rule.snoozeSchedule == null) { + return null; + } + + const now = Date.now(); + for (const snooze of rule.snoozeSchedule) { + const { duration, rRule } = snooze; + const startTimeMS = Date.parse(rRule.dtstart); + const initialEndTime = startTimeMS + duration; + // If now is during the first occurrence of the snooze + + if (now >= startTimeMS && now < initialEndTime) return new Date(initialEndTime); + + // Check to see if now is during a recurrence of the snooze + if (rRule) { + try { + const rRuleOptions = { + ...rRule, + dtstart: new Date(rRule.dtstart), + until: rRule.until ? new Date(rRule.until) : null, + wkst: rRule.wkst ? Weekday.fromStr(rRule.wkst) : null, + byweekday: rRule.byweekday ? parseByWeekday(rRule.byweekday) : null, + }; + + const recurrenceRule = new RRule(rRuleOptions); + const lastOccurrence = recurrenceRule.before(new Date(now), true); + if (!lastOccurrence) continue; + const lastOccurrenceEndTime = lastOccurrence.getTime() + duration; + if (now < lastOccurrenceEndTime) return new Date(lastOccurrenceEndTime); + } catch (e) { + throw new Error(`Failed to process RRule ${rRule}: ${e}`); + } + } + } + + return null; +} + +export function isRuleSnoozed(rule: RuleSnoozeProps) { + if (rule.muteAll) { + return true; + } + return Boolean(getRuleSnoozeEndTime(rule)); +} + +function parseByWeekday(byweekday: Array): ByWeekday[] { + const rRuleString = `RRULE:BYDAY=${byweekday.join(',')}`; + const parsedRRule = rrulestr(rRuleString); + return parsedRRule.origOptions.byweekday as ByWeekday[]; +} diff --git a/x-pack/plugins/alerting/server/routes/create_rule.ts b/x-pack/plugins/alerting/server/routes/create_rule.ts index cf044c94f2529..442162ae21cbb 100644 --- a/x-pack/plugins/alerting/server/routes/create_rule.ts +++ b/x-pack/plugins/alerting/server/routes/create_rule.ts @@ -67,12 +67,14 @@ const rewriteBodyRes: RewriteResponseCase> = ({ notifyWhen, muteAll, mutedInstanceIds, + snoozeSchedule, executionStatus: { lastExecutionDate, lastDuration, ...executionStatus }, ...rest }) => ({ ...rest, rule_type_id: alertTypeId, scheduled_task_id: scheduledTaskId, + snooze_schedule: snoozeSchedule, created_by: createdBy, updated_by: updatedBy, created_at: createdAt, diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts index f4414b0364dcb..c735d68f83bbe 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -35,7 +35,8 @@ const rewriteBodyRes: RewriteResponseCase> = ({ executionStatus, actions, scheduledTaskId, - snoozeEndTime, + snoozeSchedule, + isSnoozedUntil, ...rest }) => ({ ...rest, @@ -46,10 +47,10 @@ const rewriteBodyRes: RewriteResponseCase> = ({ updated_at: updatedAt, api_key_owner: apiKeyOwner, notify_when: notifyWhen, - mute_all: muteAll, muted_alert_ids: mutedInstanceIds, - // Remove this object spread boolean check after snoozeEndTime is added to the public API - ...(snoozeEndTime !== undefined ? { snooze_end_time: snoozeEndTime } : {}), + mute_all: muteAll, + ...(isSnoozedUntil !== undefined ? { is_snoozed_until: isSnoozedUntil } : {}), + snooze_schedule: snoozeSchedule, scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts index 537d42bbc4f47..162177d695e0a 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts @@ -21,7 +21,8 @@ export const rewriteRule = ({ executionStatus, actions, scheduledTaskId, - snoozeEndTime, + snoozeSchedule, + isSnoozedUntil, ...rest }: SanitizedRule) => ({ ...rest, @@ -35,8 +36,8 @@ export const rewriteRule = ({ mute_all: muteAll, muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, - // Remove this object spread boolean check after snoozeEndTime is added to the public API - ...(snoozeEndTime !== undefined ? { snooze_end_time: snoozeEndTime } : {}), + snooze_schedule: snoozeSchedule, + ...(isSnoozedUntil != null ? { is_snoozed_until: isSnoozedUntil } : {}), execution_status: executionStatus && { ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, diff --git a/x-pack/plugins/alerting/server/routes/update_rule.ts b/x-pack/plugins/alerting/server/routes/update_rule.ts index d2130e1f33541..1faddd66c8f0e 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.ts @@ -70,12 +70,16 @@ const rewriteBodyRes: RewriteResponseCase> = ({ muteAll, mutedInstanceIds, executionStatus, + snoozeSchedule, + isSnoozedUntil, ...rest }) => ({ ...rest, api_key_owner: apiKeyOwner, created_by: createdBy, updated_by: updatedBy, + snooze_schedule: snoozeSchedule, + ...(isSnoozedUntil ? { is_snoozed_until: isSnoozedUntil } : {}), ...(alertTypeId ? { rule_type_id: alertTypeId } : {}), ...(scheduledTaskId ? { scheduled_task_id: scheduledTaskId } : {}), ...(createdAt ? { created_at: createdAt } : {}), diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 302824221ded8..44914e3e3bce8 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -35,6 +35,7 @@ const createRulesClientMock = () => { bulkEdit: jest.fn(), snooze: jest.fn(), unsnooze: jest.fn(), + updateSnoozedUntilTime: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index ec01c2c15abf4..4e248412eae15 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -54,6 +54,7 @@ import { RuleWithLegacyId, SanitizedRuleWithLegacyId, PartialRuleWithLegacyId, + RuleSnooze, RawAlertInstance as RawAlert, } from '../types'; import { @@ -62,6 +63,7 @@ import { getRuleNotifyWhenType, validateMutatedRuleTypeParams, convertRuleIdsToKueryNode, + getRuleSnoozeEndTime, } from '../lib'; import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; import { RegistryRuleType, UntypedNormalizedRuleType } from '../rule_type_registry'; @@ -310,7 +312,8 @@ export interface CreateOptions { | 'mutedInstanceIds' | 'actions' | 'executionStatus' - | 'snoozeEndTime' + | 'snoozeSchedule' + | 'isSnoozedUntil' > & { actions: NormalizedAlertAction[] }; options?: { id?: string; @@ -391,7 +394,7 @@ export class RulesClient { private readonly fieldsToExcludeFromPublicApi: Array = [ 'monitoring', 'mapped_params', - 'snoozeEndTime', + 'snoozeSchedule', ]; constructor({ @@ -504,7 +507,8 @@ export class RulesClient { updatedBy: username, createdAt: new Date(createTime).toISOString(), updatedAt: new Date(createTime).toISOString(), - snoozeEndTime: null, + isSnoozedUntil: null, + snoozeSchedule: [], params: updatedParams as RawRule['params'], muteAll: false, mutedInstanceIds: [], @@ -1018,7 +1022,7 @@ export class RulesClient { }, snoozed: { date_range: { - field: 'alert.attributes.snoozeEndTime', + field: 'alert.attributes.snoozeSchedule.rRule.dtstart', format: 'strict_date_time', ranges: [{ from: 'now' }], }, @@ -2120,8 +2124,21 @@ export class RulesClient { // If snoozeEndTime is -1, instead mute all const newAttrs = snoozeEndTime === -1 - ? { muteAll: true, snoozeEndTime: null } - : { snoozeEndTime: new Date(snoozeEndTime).toISOString(), muteAll: false }; + ? { + muteAll: true, + snoozeSchedule: clearUnscheduledSnooze(attributes), + } + : { + snoozeSchedule: clearUnscheduledSnooze(attributes).concat({ + duration: Date.parse(snoozeEndTime) - Date.now(), + rRule: { + dtstart: new Date().toISOString(), + tzid: 'UTC', + count: 1, + }, + }), + muteAll: false, + }; const updateAttributes = this.updateMeta({ ...newAttrs, @@ -2135,7 +2152,7 @@ export class RulesClient { id, updateAttributes, updateOptions - ); + ).then(() => this.updateSnoozedUntilTime({ id })); } public async unsnooze({ id }: { id: string }): Promise { @@ -2185,7 +2202,7 @@ export class RulesClient { this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId); const updateAttributes = this.updateMeta({ - snoozeEndTime: null, + snoozeSchedule: clearUnscheduledSnooze(attributes), muteAll: false, updatedBy: await this.getUserName(), updatedAt: new Date().toISOString(), @@ -2200,6 +2217,30 @@ export class RulesClient { ); } + public async updateSnoozedUntilTime({ id }: { id: string }): Promise { + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + id + ); + + const isSnoozedUntil = getRuleSnoozeEndTime(attributes); + if (!isSnoozedUntil) return; + + const updateAttributes = this.updateMeta({ + isSnoozedUntil: isSnoozedUntil.toISOString(), + updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), + }); + const updateOptions = { version }; + + await partiallyUpdateAlert( + this.unsecuredSavedObjectsClient, + id, + updateAttributes, + updateOptions + ); + } + public async muteAll({ id }: { id: string }): Promise { return await retryIfConflicts( this.logger, @@ -2249,7 +2290,7 @@ export class RulesClient { const updateAttributes = this.updateMeta({ muteAll: true, mutedInstanceIds: [], - snoozeEndTime: null, + snoozeSchedule: clearUnscheduledSnooze(attributes), updatedBy: await this.getUserName(), updatedAt: new Date().toISOString(), }); @@ -2312,7 +2353,7 @@ export class RulesClient { const updateAttributes = this.updateMeta({ muteAll: false, mutedInstanceIds: [], - snoozeEndTime: null, + snoozeSchedule: clearUnscheduledSnooze(attributes), updatedBy: await this.getUserName(), updatedAt: new Date().toISOString(), }); @@ -2560,15 +2601,23 @@ export class RulesClient { executionStatus, schedule, actions, - snoozeEndTime, + snoozeSchedule, + isSnoozedUntil, ...partialRawRule }: Partial, references: SavedObjectReference[] | undefined, includeLegacyId: boolean = false, excludeFromPublicApi: boolean = false ): PartialRule | PartialRuleWithLegacyId { - const snoozeEndTimeDate = snoozeEndTime != null ? new Date(snoozeEndTime) : snoozeEndTime; - const includeSnoozeEndTime = snoozeEndTimeDate !== undefined && !excludeFromPublicApi; + const snoozeScheduleDates = snoozeSchedule?.map((s) => ({ + ...s, + rRule: { + ...s.rRule, + dtstart: new Date(s.rRule.dtstart), + ...(s.rRule.until ? { until: new Date(s.rRule.until) } : {}), + }, + })); + const includeSnoozeSchedule = snoozeSchedule !== undefined; const rule = { id, notifyWhen, @@ -2578,9 +2627,10 @@ export class RulesClient { schedule: schedule as IntervalSchedule, actions: actions ? this.injectReferencesIntoActions(id, actions, references || []) : [], params: this.injectReferencesIntoParams(id, ruleType, params, references || []) as Params, - ...(includeSnoozeEndTime ? { snoozeEndTime: snoozeEndTimeDate } : {}), + ...(includeSnoozeSchedule ? { snoozeSchedule: snoozeScheduleDates } : {}), ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), ...(createdAt ? { createdAt: new Date(createdAt) } : {}), + ...(isSnoozedUntil ? { isSnoozedUntil: new Date(isSnoozedUntil) } : {}), ...(scheduledTaskId ? { scheduledTaskId } : {}), ...(executionStatus ? { executionStatus: ruleExecutionStatusFromRaw(this.logger, id, executionStatus) } @@ -2795,3 +2845,9 @@ function parseDate(dateString: string | undefined, propertyName: string, default return parsedDate; } + +function clearUnscheduledSnooze(attributes: { snoozeSchedule?: RuleSnooze }) { + return attributes.snoozeSchedule + ? attributes.snoozeSchedule.filter((s) => typeof s.id !== 'undefined') + : []; +} diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index 1a3d203162bd6..bc1c8d276aedd 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -203,7 +203,7 @@ describe('aggregate()', () => { }, snoozed: { date_range: { - field: 'alert.attributes.snoozeEndTime', + field: 'alert.attributes.snoozeSchedule.rRule.dtstart', format: 'strict_date_time', ranges: [{ from: 'now' }], }, @@ -240,7 +240,7 @@ describe('aggregate()', () => { }, snoozed: { date_range: { - field: 'alert.attributes.snoozeEndTime', + field: 'alert.attributes.snoozeSchedule.rRule.dtstart', format: 'strict_date_time', ranges: [{ from: 'now' }], }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 8e24b7c183262..f5c839c5006fd 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -300,7 +300,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], actions: [ { @@ -376,6 +376,7 @@ describe('create()', () => { "interval": "1m", }, "scheduledTaskId": "task-123", + "snoozeSchedule": Array [], "tags": Array [ "foo", ], @@ -412,6 +413,7 @@ describe('create()', () => { "status": "pending", "warning": null, }, + "isSnoozedUntil": null, "legacyId": null, "meta": Object { "versionApiKeyLastmodified": "v8.0.0", @@ -434,7 +436,7 @@ describe('create()', () => { "schedule": Object { "interval": "1m", }, - "snoozeEndTime": null, + "snoozeSchedule": Array [], "tags": Array [ "foo", ], @@ -506,7 +508,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], actions: [ { @@ -566,7 +568,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], actions: [ { @@ -618,6 +620,7 @@ describe('create()', () => { "status": "pending", "warning": null, }, + "isSnoozedUntil": null, "legacyId": "123", "meta": Object { "versionApiKeyLastmodified": "v7.10.0", @@ -640,7 +643,7 @@ describe('create()', () => { "schedule": Object { "interval": "1m", }, - "snoozeEndTime": null, + "snoozeSchedule": Array [], "tags": Array [ "foo", ], @@ -1044,6 +1047,7 @@ describe('create()', () => { createdAt: '2019-02-12T21:01:22.479Z', createdBy: 'elastic', enabled: true, + isSnoozedUntil: null, legacyId: null, executionStatus: { error: null, @@ -1054,7 +1058,7 @@ describe('create()', () => { monitoring: getDefaultRuleMonitoring(), meta: { versionApiKeyLastmodified: kibanaVersion }, muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], name: 'abc', notifyWhen: 'onActiveAlert', @@ -1243,6 +1247,7 @@ describe('create()', () => { createdAt: '2019-02-12T21:01:22.479Z', createdBy: 'elastic', enabled: true, + isSnoozedUntil: null, legacyId: null, executionStatus: { error: null, @@ -1253,7 +1258,7 @@ describe('create()', () => { monitoring: getDefaultRuleMonitoring(), meta: { versionApiKeyLastmodified: kibanaVersion }, muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], name: 'abc', notifyWhen: 'onActiveAlert', @@ -1407,6 +1412,7 @@ describe('create()', () => { alertTypeId: '123', apiKey: null, apiKeyOwner: null, + isSnoozedUntil: null, legacyId: null, consumer: 'bar', createdAt: '2019-02-12T21:01:22.479Z', @@ -1421,7 +1427,7 @@ describe('create()', () => { monitoring: getDefaultRuleMonitoring(), meta: { versionApiKeyLastmodified: kibanaVersion }, muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], name: 'abc', notifyWhen: 'onActiveAlert', @@ -1530,7 +1536,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], notifyWhen: 'onActionGroupChange', actions: [ @@ -1571,6 +1577,7 @@ describe('create()', () => { alertTypeId: '123', consumer: 'bar', name: 'abc', + isSnoozedUntil: null, legacyId: null, params: { bar: true }, apiKey: null, @@ -1587,7 +1594,7 @@ describe('create()', () => { throttle: '10m', notifyWhen: 'onActionGroupChange', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -1638,6 +1645,7 @@ describe('create()', () => { "interval": "1m", }, "scheduledTaskId": "task-123", + "snoozeSchedule": Array [], "tags": Array [ "foo", ], @@ -1662,7 +1670,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], notifyWhen: 'onThrottleInterval', actions: [ @@ -1700,6 +1708,7 @@ describe('create()', () => { params: { foo: true }, }, ], + isSnoozedUntil: null, legacyId: null, alertTypeId: '123', consumer: 'bar', @@ -1719,7 +1728,7 @@ describe('create()', () => { throttle: '10m', notifyWhen: 'onThrottleInterval', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -1770,6 +1779,7 @@ describe('create()', () => { "interval": "1m", }, "scheduledTaskId": "task-123", + "snoozeSchedule": Array [], "tags": Array [ "foo", ], @@ -1794,7 +1804,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], notifyWhen: 'onActiveAlert', actions: [ @@ -1832,6 +1842,7 @@ describe('create()', () => { params: { foo: true }, }, ], + isSnoozedUntil: null, legacyId: null, alertTypeId: '123', consumer: 'bar', @@ -1851,7 +1862,7 @@ describe('create()', () => { throttle: null, notifyWhen: 'onActiveAlert', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -1902,6 +1913,7 @@ describe('create()', () => { "interval": "1m", }, "scheduledTaskId": "task-123", + "snoozeSchedule": Array [], "tags": Array [ "foo", ], @@ -1935,7 +1947,7 @@ describe('create()', () => { updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], actions: [ { @@ -1993,13 +2005,14 @@ describe('create()', () => { ], apiKeyOwner: null, apiKey: null, + isSnoozedUntil: null, legacyId: null, createdBy: 'elastic', updatedBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], executionStatus: { status: 'pending', @@ -2066,6 +2079,7 @@ describe('create()', () => { "interval": "10s", }, "scheduledTaskId": "task-123", + "snoozeSchedule": Array [], "tags": Array [ "foo", ], @@ -2345,6 +2359,7 @@ describe('create()', () => { alertTypeId: '123', consumer: 'bar', name: 'abc', + isSnoozedUntil: null, legacyId: null, params: { bar: true }, apiKey: Buffer.from('123:abc').toString('base64'), @@ -2361,7 +2376,7 @@ describe('create()', () => { throttle: null, notifyWhen: 'onActiveAlert', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], tags: ['foo'], executionStatus: { @@ -2444,6 +2459,7 @@ describe('create()', () => { params: { foo: true }, }, ], + isSnoozedUntil: null, legacyId: null, alertTypeId: '123', consumer: 'bar', @@ -2463,7 +2479,7 @@ describe('create()', () => { throttle: null, notifyWhen: 'onActiveAlert', muteAll: false, - snoozeEndTime: null, + snoozeSchedule: [], mutedInstanceIds: [], tags: ['foo'], executionStatus: { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts index 7f8ae28a20c6e..e2625be88482c 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts @@ -82,7 +82,7 @@ describe('muteAll()', () => { { muteAll: true, mutedInstanceIds: [], - snoozeEndTime: null, + snoozeSchedule: [], updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts index cf063eea07862..f5d4cb372f867 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts @@ -82,7 +82,7 @@ describe('unmuteAll()', () => { { muteAll: false, mutedInstanceIds: [], - snoozeEndTime: null, + snoozeSchedule: [], updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 6566fee15d4a8..f4f23cced722c 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -30,7 +30,8 @@ export const AlertAttributesExcludedFromAAD = [ 'updatedAt', 'executionStatus', 'monitoring', - 'snoozeEndTime', + 'snoozeSchedule', + 'isSnoozedUntil', ]; // useful for Pick which is a @@ -45,7 +46,8 @@ export type AlertAttributesExcludedFromAADType = | 'updatedAt' | 'executionStatus' | 'monitoring' - | 'snoozeEndTime'; + | 'snoozeSchedule' + | 'isSnoozedUntil'; export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.ts b/x-pack/plugins/alerting/server/saved_objects/mappings.ts index 5e2803222ecba..31ad40117a7ec 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.ts +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.ts @@ -185,7 +185,73 @@ export const alertMappings: SavedObjectsTypeMappingDefinition = { }, }, }, - snoozeEndTime: { + snoozeSchedule: { + type: 'nested', + properties: { + id: { + type: 'keyword', + }, + duration: { + type: 'long', + }, + rRule: { + type: 'nested', + properties: { + freq: { + type: 'keyword', + }, + dtstart: { + type: 'date', + format: 'strict_date_time', + }, + tzid: { + type: 'keyword', + }, + until: { + type: 'date', + format: 'strict_date_time', + }, + count: { + type: 'long', + }, + interval: { + type: 'long', + }, + wkst: { + type: 'keyword', + }, + byweekday: { + type: 'keyword', + }, + bymonth: { + type: 'short', + }, + bysetpos: { + type: 'long', + }, + bymonthday: { + type: 'short', + }, + byyearday: { + type: 'short', + }, + byweekno: { + type: 'short', + }, + byhour: { + type: 'long', + }, + byminute: { + type: 'long', + }, + bysecond: { + type: 'long', + }, + }, + }, + }, + }, + isSnoozedUntil: { type: 'date', format: 'strict_date_time', }, diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index c83d0a95dfdcb..bbf93f85450cb 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import sinon from 'sinon'; import uuid from 'uuid'; import { getMigrations, isAnyActionSupportIncidents } from './migrations'; import { RawRule } from '../types'; @@ -2318,6 +2319,27 @@ describe('successful migrations', () => { }); describe('8.3.0', () => { + test('migrates snoozed rules to the new data model', () => { + const fakeTimer = sinon.useFakeTimers(); + const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.3.0' + ]; + const mutedAlert = getMockData( + { + snoozeEndTime: '1970-01-02T00:00:00.000Z', + }, + true + ); + const migratedMutedAlert830 = migration830(mutedAlert, migrationContext); + + expect(migratedMutedAlert830.attributes.snoozeSchedule.length).toEqual(1); + expect(migratedMutedAlert830.attributes.snoozeSchedule[0].rRule.dtstart).toEqual( + '1970-01-01T00:00:00.000Z' + ); + expect(migratedMutedAlert830.attributes.snoozeSchedule[0].duration).toEqual(86400000); + fakeTimer.restore(); + }); + test('migrates es_query alert params', () => { const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ '8.3.0' diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index b3f8d873d8ef0..ddae200ae8fa6 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -7,6 +7,8 @@ import { isRuleType, ruleTypeMappings } from '@kbn/securitysolution-rules'; import { isString } from 'lodash/fp'; +import { omit } from 'lodash'; +import moment from 'moment-timezone'; import { gte } from 'semver'; import { LogMeta, @@ -164,7 +166,7 @@ export function getMigrations( const migrationRules830 = createEsoMigration( encryptedSavedObjects, (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(addSearchType, removeInternalTags) + pipeMigrations(addSearchType, removeInternalTags, convertSnoozes) ); return mergeSavedObjectMigrationMaps( @@ -888,6 +890,33 @@ function addMappedParams( return doc; } +function convertSnoozes( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { snoozeEndTime }, + } = doc; + + return { + ...doc, + attributes: { + ...(omit(doc.attributes, ['snoozeEndTime']) as RawRule), + snoozeSchedule: snoozeEndTime + ? [ + { + duration: Date.parse(snoozeEndTime as string) - Date.now(), + rRule: { + dtstart: new Date().toISOString(), + tzid: moment.tz.guess(), + count: 1, + }, + }, + ] + : [], + }, + }; +} + function getCorrespondingAction( actions: SavedObjectAttribute, connectorRef: string diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 7d95f63f3c43c..f3d2c7039585b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -470,17 +470,42 @@ describe('Task Runner', () => { const snoozeTestParams: SnoozeTestParams[] = [ [false, null, false], [false, undefined, false], - [false, DATE_1970, false], - [false, DATE_9999, true], + // Stringify the snooze schedules for better failure reporting + [ + false, + JSON.stringify([ + { rRule: { dtstart: DATE_9999, tzid: 'UTC', count: 1 }, duration: 100000000 }, + ]), + false, + ], + [ + false, + JSON.stringify([ + { rRule: { dtstart: DATE_1970, tzid: 'UTC', count: 1 }, duration: 100000000 }, + ]), + true, + ], [true, null, true], [true, undefined, true], - [true, DATE_1970, true], - [true, DATE_9999, true], + [ + true, + JSON.stringify([ + { rRule: { dtstart: DATE_9999, tzid: 'UTC', count: 1 }, duration: 100000000 }, + ]), + true, + ], + [ + true, + JSON.stringify([ + { rRule: { dtstart: DATE_1970, tzid: 'UTC', count: 1 }, duration: 100000000 }, + ]), + true, + ], ]; test.each(snoozeTestParams)( - 'snoozing works as expected with muteAll: %s; snoozeEndTime: %s', - async (muteAll, snoozeEndTime, shouldBeSnoozed) => { + 'snoozing works as expected with muteAll: %s; snoozeSchedule: %s', + async (muteAll, snoozeSchedule, shouldBeSnoozed) => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); ruleType.executor.mockImplementation( @@ -507,7 +532,7 @@ describe('Task Runner', () => { rulesClient.get.mockResolvedValue({ ...mockedRuleTypeSavedObject, muteAll, - snoozeEndTime: snoozeEndTime != null ? new Date(snoozeEndTime) : snoozeEndTime, + snoozeSchedule: snoozeSchedule != null ? JSON.parse(snoozeSchedule) : [], }); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 6cd6b73b9539e..525c252b40b66 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -25,6 +25,7 @@ import { getRecoveredAlerts, ruleExecutionStatusToRaw, validateRuleTypeParams, + isRuleSnoozed, } from '../lib'; import { Rule, @@ -247,18 +248,6 @@ export class TaskRunner< } } - private isRuleSnoozed(rule: SanitizedRule): boolean { - if (rule.muteAll) { - return true; - } - - if (rule.snoozeEndTime == null) { - return false; - } - - return Date.now() < rule.snoozeEndTime.getTime(); - } - private shouldLogAndScheduleActionsForAlerts() { // if execution hasn't been cancelled, return true if (!this.cancelled) { @@ -477,7 +466,10 @@ export class TaskRunner< }); } - const ruleIsSnoozed = this.isRuleSnoozed(rule); + const ruleIsSnoozed = isRuleSnoozed(rule); + if (ruleIsSnoozed) { + this.markRuleAsSnoozed(rule.id); + } if (!ruleIsSnoozed && this.shouldLogAndScheduleActionsForAlerts()) { const mutedAlertIdsSet = new Set(mutedInstanceIds); @@ -580,6 +572,23 @@ export class TaskRunner< return this.executeRule(fakeRequest, rule, validatedParams, executionHandler, spaceId); } + private async markRuleAsSnoozed(id: string) { + let apiKey: string | null; + + const { + params: { alertId: ruleId, spaceId }, + } = this.taskInstance; + try { + const decryptedAttributes = await this.getDecryptedAttributes(ruleId, spaceId); + apiKey = decryptedAttributes.apiKey; + } catch (err) { + throw new ErrorWithReason(RuleExecutionStatusErrorReasons.Decrypt, err); + } + const fakeRequest = this.getFakeKibanaRequest(spaceId, apiKey); + const rulesClient = this.context.getRulesClientWithRequest(fakeRequest); + await rulesClient.updateSnoozedUntilTime({ id }); + } + private async loadRuleAttributesAndRun(): Promise> { const { params: { alertId: ruleId, spaceId }, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 1c453df386e24..7b1725e42bd5e 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -40,6 +40,7 @@ import { SanitizedRuleConfig, RuleMonitoring, MappedParams, + RuleSnooze, } from '../common'; export type WithoutQueryAndParams = Pick>; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; @@ -249,7 +250,8 @@ export interface RawRule extends SavedObjectAttributes { meta?: RuleMeta; executionStatus: RawRuleExecutionStatus; monitoring?: RuleMonitoring; - snoozeEndTime?: string | null; // Remove ? when this parameter is made available in the public API + snoozeSchedule?: RuleSnooze; // Remove ? when this parameter is made available in the public API + isSnoozedUntil?: string | null; } export interface AlertingPlugin { diff --git a/x-pack/plugins/fleet/public/components/package_icon.tsx b/x-pack/plugins/fleet/public/components/package_icon.tsx index 9e7b54673c9d9..7d106852324fe 100644 --- a/x-pack/plugins/fleet/public/components/package_icon.tsx +++ b/x-pack/plugins/fleet/public/components/package_icon.tsx @@ -16,7 +16,8 @@ export const PackageIcon: React.FunctionComponent< UsePackageIconType & Omit > = ({ packageName, integrationName, version, icons, tryApi, ...euiIconProps }) => { const iconType = usePackageIconType({ packageName, integrationName, version, icons, tryApi }); - return ; + // @ts-expect-error loading="lazy" is not supported by EuiIcon + return ; }; export const CardIcon: React.FunctionComponent> = ( @@ -26,7 +27,8 @@ export const CardIcon: React.FunctionComponent; } else if (icons && icons.length === 1 && icons[0].type === 'svg') { - return ; + // @ts-expect-error loading="lazy" is not supported by EuiIcon + return ; } else { return ; } diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/layer_template.tsx index 7e40f37dce26f..4edf85bc922d1 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/layer_template.tsx +++ b/x-pack/plugins/maps/public/classes/layers/wizards/choropleth_layer_wizard/layer_template.tsx @@ -121,10 +121,15 @@ export class LayerTemplate extends Component { }; _loadEmsFileFields = async () => { - const emsFileLayers = await getEmsFileLayers(); - const emsFileLayer = emsFileLayers.find((fileLayer: FileLayer) => { - return fileLayer.getId() === this.state.leftEmsFileId; - }); + let emsFileLayer: FileLayer | undefined; + try { + const emsFileLayers = await getEmsFileLayers(); + emsFileLayer = emsFileLayers.find((fileLayer: FileLayer) => { + return fileLayer.getId() === this.state.leftEmsFileId; + }); + } catch (error) { + // ignore error, lack of EMS file layers will be surfaced in EMS file select + } if (!this._isMounted || !emsFileLayer) { return; diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx index c2f86a2cdb161..09c6a0bf313b0 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx @@ -45,25 +45,31 @@ export class TileServiceSelect extends Component { } _loadTmsOptions = async () => { - const emsTMSServices = await getEmsTmsServices(); - - if (!this._isMounted) { - return; + try { + const emsTMSServices = await getEmsTmsServices(); + + if (!this._isMounted) { + return; + } + + const emsTmsOptions = emsTMSServices.map((tmsService) => { + return { + value: tmsService.getId(), + text: tmsService.getDisplayName() ? tmsService.getDisplayName() : tmsService.getId(), + }; + }); + emsTmsOptions.unshift({ + value: AUTO_SELECT, + text: i18n.translate('xpack.maps.source.emsTile.autoLabel', { + defaultMessage: 'Autoselect based on Kibana theme', + }), + }); + this.setState({ emsTmsOptions, hasLoaded: true }); + } catch (error) { + if (this._isMounted) { + this.setState({ emsTmsOptions: [], hasLoaded: true }); + } } - - const emsTmsOptions = emsTMSServices.map((tmsService) => { - return { - value: tmsService.getId(), - text: tmsService.getDisplayName() ? tmsService.getDisplayName() : tmsService.getId(), - }; - }); - emsTmsOptions.unshift({ - value: AUTO_SELECT, - text: i18n.translate('xpack.maps.source.emsTile.autoLabel', { - defaultMessage: 'Autoselect based on Kibana theme', - }), - }); - this.setState({ emsTmsOptions, hasLoaded: true }); }; _onChange = (e: ChangeEvent) => { diff --git a/x-pack/plugins/maps/public/components/ems_file_select.tsx b/x-pack/plugins/maps/public/components/ems_file_select.tsx index 694e3f6413059..f2a409b8629b0 100644 --- a/x-pack/plugins/maps/public/components/ems_file_select.tsx +++ b/x-pack/plugins/maps/public/components/ems_file_select.tsx @@ -33,7 +33,18 @@ export class EMSFileSelect extends Component { }; _loadFileOptions = async () => { - const fileLayers: FileLayer[] = await getEmsFileLayers(); + let fileLayers: FileLayer[] = []; + try { + fileLayers = await getEmsFileLayers(); + } catch (error) { + if (this._isMounted) { + this.setState({ + hasLoadedOptions: true, + emsFileOptions: [], + }); + } + } + const options = fileLayers.map((fileLayer) => { return { value: fileLayer.getId(), diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts index b88305cae0e92..4ade37658fd13 100644 --- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts @@ -33,8 +33,13 @@ interface FileLayerFieldShim { export async function suggestEMSTermJoinConfig( sampleValuesConfig: SampleValuesConfig ): Promise { - const fileLayers = await getEmsFileLayers(); - return emsAutoSuggest(sampleValuesConfig, fileLayers); + try { + const fileLayers = await getEmsFileLayers(); + return emsAutoSuggest(sampleValuesConfig, fileLayers); + } catch (error) { + // can not return suggestions since EMS is not available. + return null; + } } export function emsAutoSuggest( diff --git a/x-pack/plugins/maps/public/lens/choropleth_chart/expression_renderer.tsx b/x-pack/plugins/maps/public/lens/choropleth_chart/expression_renderer.tsx index 4fc96d7625504..b2705ea5f0492 100644 --- a/x-pack/plugins/maps/public/lens/choropleth_chart/expression_renderer.tsx +++ b/x-pack/plugins/maps/public/lens/choropleth_chart/expression_renderer.tsx @@ -10,6 +10,7 @@ import ReactDOM from 'react-dom'; import type { IInterpreterRenderHandlers } from '@kbn/expressions-plugin/public'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import type { CoreSetup, CoreStart } from '@kbn/core/public'; +import type { FileLayer } from '@elastic/ems-client'; import type { MapsPluginStartDependencies } from '../../plugin'; import type { ChoroplethChartProps } from './types'; import type { MapEmbeddableInput, MapEmbeddableOutput } from '../../embeddable'; @@ -40,12 +41,19 @@ export function getExpressionRenderer(coreSetup: CoreSetup, domNode, diff --git a/x-pack/plugins/maps/public/lens/choropleth_chart/setup.ts b/x-pack/plugins/maps/public/lens/choropleth_chart/setup.ts index 626030e72a576..3e1525353d1b5 100644 --- a/x-pack/plugins/maps/public/lens/choropleth_chart/setup.ts +++ b/x-pack/plugins/maps/public/lens/choropleth_chart/setup.ts @@ -8,6 +8,7 @@ import type { ExpressionsSetup } from '@kbn/expressions-plugin/public'; import type { CoreSetup, CoreStart } from '@kbn/core/public'; import type { LensPublicSetup } from '@kbn/lens-plugin/public'; +import type { FileLayer } from '@elastic/ems-client'; import type { MapsPluginStartDependencies } from '../../plugin'; import { getExpressionFunction } from './expression_function'; import { getExpressionRenderer } from './expression_renderer'; @@ -28,9 +29,17 @@ export function setupLensChoroplethChart( await coreSetup.getStartServices(); const { getEmsFileLayers } = await import('../../util'); const { getVisualization } = await import('./visualization'); + + let emsFileLayers: FileLayer[] = []; + try { + emsFileLayers = await getEmsFileLayers(); + } catch (error) { + // ignore error, lack of EMS file layers will be surfaced in dimension editor + } + return getVisualization({ theme: coreStart.theme, - emsFileLayers: await getEmsFileLayers(), + emsFileLayers, paletteService: await plugins.charts.palettes.getPalettes(), }); }); diff --git a/x-pack/plugins/observability/public/pages/rules/config.ts b/x-pack/plugins/observability/public/pages/rules/config.ts index 8c39acb75976d..4e7b9e83d5ab1 100644 --- a/x-pack/plugins/observability/public/pages/rules/config.ts +++ b/x-pack/plugins/observability/public/pages/rules/config.ts @@ -49,8 +49,8 @@ export const OBSERVABILITY_RULE_TYPES = [ 'xpack.uptime.alerts.durationAnomaly', 'apm.error_rate', 'apm.transaction_error_rate', + 'apm.anomaly', 'apm.transaction_duration', - 'apm.transaction_duration_anomaly', 'metrics.alert.inventory.threshold', 'metrics.alert.threshold', 'logs.alert.document.count', diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index 5bd4bf0fa3f52..bed0f49fa1b59 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -295,4 +295,92 @@ describe('Config Deprecations', () => { ] `); }); + + it(`warns that 'xpack.security.authc.providers.anonymous..credentials.apiKey' is deprecated`, () => { + const config = { + xpack: { + security: { + authc: { + providers: { + anonymous: { + anonymous1: { + credentials: { + apiKey: 'VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==', + }, + }, + }, + }, + }, + }, + }, + }; + const { messages, configPaths } = applyConfigDeprecations(cloneDeep(config)); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Support for apiKey is being removed from the 'anonymous' authentication provider. Use username and password credentials.", + ] + `); + + expect(configPaths).toEqual([ + 'xpack.security.authc.providers.anonymous.anonymous1.credentials.apiKey', + ]); + }); + + it(`warns that 'xpack.security.authc.providers.anonymous..credentials.apiKey' with id and key is deprecated`, () => { + const config = { + xpack: { + security: { + authc: { + providers: { + anonymous: { + anonymous1: { + credentials: { + apiKey: { id: 'VuaCfGcBCdbkQm-e5aOx', key: 'ui2lp2axTNmsyakw9tvNnw' }, + }, + }, + }, + }, + }, + }, + }, + }; + const { messages, configPaths } = applyConfigDeprecations(cloneDeep(config)); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Support for apiKey is being removed from the 'anonymous' authentication provider. Use username and password credentials.", + ] + `); + + expect(configPaths).toEqual([ + 'xpack.security.authc.providers.anonymous.anonymous1.credentials.apiKey', + ]); + }); + + it(`warns that 'xpack.security.authc.providers.anonymous..credentials' of 'elasticsearch_anonymous_user' is deprecated`, () => { + const config = { + xpack: { + security: { + authc: { + providers: { + anonymous: { + anonymous1: { + credentials: 'elasticsearch_anonymous_user', + }, + }, + }, + }, + }, + }, + }; + const { messages, configPaths } = applyConfigDeprecations(cloneDeep(config)); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Support for 'elasticsearch_anonymous_user' is being removed from the 'anonymous' authentication provider. Use username and password credentials.", + ] + `); + + expect(configPaths).toEqual([ + 'xpack.security.authc.providers.anonymous.anonymous1.credentials', + ]); + }); }); diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index b4625c521e036..262a2f885779b 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -35,7 +35,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ (settings, _fromPath, addDeprecation, { branch }) => { if (Array.isArray(settings?.xpack?.security?.authc?.providers)) { // TODO: remove when docs support "main" - const docsBranch = branch === 'main' ? 'master' : 'main'; + const docsBranch = branch === 'main' ? 'master' : 'branch'; addDeprecation({ configPath: 'xpack.security.authc.providers', title: i18n.translate('xpack.security.deprecations.authcProvidersTitle', { @@ -62,7 +62,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }, (settings, _fromPath, addDeprecation, { branch }) => { // TODO: remove when docs support "main" - const docsBranch = branch === 'main' ? 'master' : 'main'; + const docsBranch = branch === 'main' ? 'master' : 'branch'; const hasProviderType = (providerType: string) => { const providers = settings?.xpack?.security?.authc?.providers; @@ -106,7 +106,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }, (settings, _fromPath, addDeprecation, { branch }) => { // TODO: remove when docs support "main" - const docsBranch = branch === 'main' ? 'master' : 'main'; + const docsBranch = branch === 'main' ? 'master' : 'branch'; const samlProviders = (settings?.xpack?.security?.authc?.providers?.saml ?? {}) as Record< string, any @@ -138,4 +138,57 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }); } }, + (settings, _fromPath, addDeprecation, { branch }) => { + // TODO: remove when docs support "main" + const docsBranch = branch === 'main' ? 'master' : 'branch'; + const anonProviders = (settings?.xpack?.security?.authc?.providers?.anonymous ?? {}) as Record< + string, + any + >; + + const credTypeElasticsearchAnonUser = 'elasticsearch_anonymous_user'; + const credTypeApiKey = 'apiKey'; + + for (const provider of Object.entries(anonProviders)) { + if ( + !!provider[1].credentials.apiKey || + provider[1].credentials === credTypeElasticsearchAnonUser + ) { + const isApiKey: boolean = !!provider[1].credentials.apiKey; + addDeprecation({ + configPath: `xpack.security.authc.providers.anonymous.${provider[0]}.credentials${ + isApiKey ? '.apiKey' : '' + }`, + title: i18n.translate( + 'xpack.security.deprecations.anonymousApiKeyOrElasticsearchAnonUserTitle', + { + values: { + credType: isApiKey ? `${credTypeApiKey}` : `'${credTypeElasticsearchAnonUser}'`, + }, + defaultMessage: `Use of {credType} for "xpack.security.authc.providers.anonymous.credentials" is deprecated.`, + } + ), + message: i18n.translate( + 'xpack.security.deprecations.anonymousApiKeyOrElasticsearchAnonUserMessage', + { + values: { + credType: isApiKey ? `${credTypeApiKey}` : `'${credTypeElasticsearchAnonUser}'`, + }, + defaultMessage: `Support for {credType} is being removed from the 'anonymous' authentication provider. Use username and password credentials.`, + } + ), + level: 'warning', + documentationUrl: `https://www.elastic.co/guide/en/kibana/${docsBranch}/kibana-authentication.html#anonymous-authentication`, + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.security.deprecations.anonAuthCredentials.manualSteps1', { + defaultMessage: + 'Change the anonymous authentication provider to use username and password credentials.', + }), + ], + }, + }); + } + } + }, ]; diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index 3b6e28765f69f..15a0713d80326 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -49,6 +49,7 @@ describe('Security UsageCollector', () => { sessionIdleTimeoutInMinutes: 480, sessionLifespanInMinutes: 43200, sessionCleanupInMinutes: 60, + anonymousCredentialType: undefined, }; describe('initialization', () => { @@ -109,6 +110,7 @@ describe('Security UsageCollector', () => { sessionIdleTimeoutInMinutes: 0, sessionLifespanInMinutes: 0, sessionCleanupInMinutes: 0, + anonymousCredentialType: undefined, }); }); @@ -465,4 +467,197 @@ describe('Security UsageCollector', () => { }); }); }); + + describe('anonymous auth credentials', () => { + it('reports anonymous credential of apiKey with id and key as api_key', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 1, + credentials: { + apiKey: { id: 'VuaCfGcBCdbkQm-e5aOx', key: 'ui2lp2axTNmsyakw9tvNnw' }, + }, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(collectorFetchContext); + + expect(usage).toEqual({ + ...DEFAULT_USAGE, + enabledAuthProviders: ['anonymous'], + anonymousCredentialType: 'api_key', + }); + }); + + it('reports anonymous credential of apiKey as api_key', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 1, + credentials: { + apiKey: 'VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==', + }, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(collectorFetchContext); + + expect(usage).toEqual({ + ...DEFAULT_USAGE, + enabledAuthProviders: ['anonymous'], + anonymousCredentialType: 'api_key', + }); + }); + + it(`reports anonymous credential of 'elasticsearch_anonymous_user' as elasticsearch_anonymous_user`, async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 1, + credentials: 'elasticsearch_anonymous_user', + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(collectorFetchContext); + + expect(usage).toEqual({ + ...DEFAULT_USAGE, + enabledAuthProviders: ['anonymous'], + anonymousCredentialType: 'elasticsearch_anonymous_user', + }); + }); + + it('reports anonymous credential of username and password as usernanme_password', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 1, + credentials: { + username: 'anonymous_service_account', + password: 'anonymous_service_account_password', + }, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(collectorFetchContext); + + expect(usage).toEqual({ + ...DEFAULT_USAGE, + enabledAuthProviders: ['anonymous'], + anonymousCredentialType: 'username_password', + }); + }); + + it('reports lack of anonymous credential as undefined', async () => { + const config = createSecurityConfig(ConfigSchema.validate({})); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(collectorFetchContext); + + expect(usage).toEqual({ + ...DEFAULT_USAGE, + enabledAuthProviders: ['basic'], + anonymousCredentialType: undefined, + }); + }); + + it('reports the enabled anonymous credential of username and password as usernanme_password', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + anonymous: { + anonymous1: { + order: 1, + enabled: false, + credentials: { + apiKey: 'VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==', + }, + }, + anonymous2: { + order: 2, + credentials: { + username: 'anonymous_service_account', + password: 'anonymous_service_account_password', + }, + }, + anonymous3: { + order: 3, + enabled: false, + credentials: { + apiKey: { id: 'VuaCfGcBCdbkQm-e5aOx', key: 'ui2lp2axTNmsyakw9tvNnw' }, + }, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(collectorFetchContext); + + expect(usage).toEqual({ + ...DEFAULT_USAGE, + enabledAuthProviders: ['anonymous'], + anonymousCredentialType: 'username_password', + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts index 0b1ef3a3d1f39..4050e70bbcfed 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -20,6 +20,7 @@ interface Usage { sessionIdleTimeoutInMinutes: number; sessionLifespanInMinutes: number; sessionCleanupInMinutes: number; + anonymousCredentialType: string | undefined; } interface Deps { @@ -122,6 +123,13 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens 'The session cleanup interval that is configured, in minutes (0 if disabled).', }, }, + anonymousCredentialType: { + type: 'keyword', + _meta: { + description: + 'The credential type that is configured for the anonymous authentication provider.', + }, + }, }, fetch: () => { const { allowRbac, allowAccessAgreement, allowAuditLogging } = license.getFeatures(); @@ -136,6 +144,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens sessionIdleTimeoutInMinutes: 0, sessionLifespanInMinutes: 0, sessionCleanupInMinutes: 0, + anonymousCredentialType: undefined, }; } @@ -163,6 +172,24 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens const sessionLifespanInMinutes = sessionExpirations.lifespan?.asMinutes() ?? 0; const sessionCleanupInMinutes = config.session.cleanupInterval?.asMinutes() ?? 0; + const anonProviders = config.authc.providers.anonymous ?? ({} as Record); + const foundProvider = Object.entries(anonProviders).find( + ([_, provider]) => !!provider.credentials && provider.enabled + ); + + const credElasticAnonUser = 'elasticsearch_anonymous_user'; + const credApiKey = 'api_key'; + const credUsernamePassword = 'username_password'; + + let anonymousCredentialType; + if (foundProvider) { + if (!!foundProvider[1].credentials.apiKey) anonymousCredentialType = credApiKey; + else if (foundProvider[1].credentials === credElasticAnonUser) + anonymousCredentialType = credElasticAnonUser; + else if (!!foundProvider[1].credentials.username && !!foundProvider[1].credentials.password) + anonymousCredentialType = credUsernamePassword; + } + return { auditLoggingEnabled, loginSelectorEnabled, @@ -173,6 +200,7 @@ export function registerSecurityUsageCollector({ usageCollection, config, licens sessionIdleTimeoutInMinutes, sessionLifespanInMinutes, sessionCleanupInMinutes, + anonymousCredentialType, }; }, }); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 17da07280a7f0..f8c159241d00e 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -108,6 +108,7 @@ export enum SecurityPageName { overview = 'overview', policies = 'policy', rules = 'rules', + rulesCreate = 'rules-create', timelines = 'timelines', timelinesTemplates = 'timelines-templates', trustedApps = 'trusted_apps', @@ -119,7 +120,7 @@ export enum SecurityPageName { sessions = 'sessions', usersEvents = 'users-events', usersExternalAlerts = 'users-external_alerts', - threatHuntingLanding = 'threat-hunting', + threatHuntingLanding = 'threat_hunting', dashboardsLanding = 'dashboards', } @@ -134,6 +135,7 @@ export const DETECTION_RESPONSE_PATH = '/detection_response' as const; export const DETECTIONS_PATH = '/detections' as const; export const ALERTS_PATH = '/alerts' as const; export const RULES_PATH = '/rules' as const; +export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const; export const EXCEPTIONS_PATH = '/exceptions' as const; export const HOSTS_PATH = '/hosts' as const; export const USERS_PATH = '/users' as const; diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 8d8871305b034..550ec608a76cb 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -35,6 +35,7 @@ import { GETTING_STARTED, THREAT_HUNTING, DASHBOARDS, + CREATE_NEW_RULE, } from '../translations'; import { OVERVIEW_PATH, @@ -59,6 +60,7 @@ import { THREAT_HUNTING_PATH, DASHBOARDS_PATH, MANAGE_PATH, + RULES_CREATE_PATH, } from '../../../common/constants'; import { ExperimentalFeatures } from '../../../common/experimental_features'; @@ -183,6 +185,15 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ }), ], searchable: true, + deepLinks: [ + { + id: SecurityPageName.rulesCreate, + title: CREATE_NEW_RULE, + path: RULES_CREATE_PATH, + navLinkStatus: AppNavLinkStatus.hidden, + searchable: false, + }, + ], }, { id: SecurityPageName.exceptions, diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index aa7eaa83685db..9857e7160a209 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -110,6 +110,10 @@ export const BLOCKLIST = i18n.translate('xpack.securitySolution.navigation.block defaultMessage: 'Blocklist', }); +export const CREATE_NEW_RULE = i18n.translate('xpack.securitySolution.navigation.newRuleTitle', { + defaultMessage: 'Create new rule', +}); + export const GO_TO_DOCUMENTATION = i18n.translate( 'xpack.securitySolution.goToDocumentationButton', { diff --git a/x-pack/plugins/security_solution/public/cases/links.ts b/x-pack/plugins/security_solution/public/cases/links.ts index 3765dfadc8fcc..9ed7a1f3980a6 100644 --- a/x-pack/plugins/security_solution/public/cases/links.ts +++ b/x-pack/plugins/security_solution/public/cases/links.ts @@ -21,9 +21,11 @@ export const getCasesLinkItems = (): LinkItem => { [SecurityPageName.caseConfigure]: { features: [FEATURE.casesCrud], licenseType: 'gold', + hideTimeline: true, }, [SecurityPageName.caseCreate]: { features: [FEATURE.casesCrud], + hideTimeline: true, }, }, }); diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx index b66d923cf0a15..7a6ddbec9e88b 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx @@ -11,8 +11,6 @@ export const getDetectionEngineUrl = (search?: string) => `${appendSearch(search export const getRulesUrl = (search?: string) => `${appendSearch(search)}`; -export const getCreateRuleUrl = (search?: string) => `/create${appendSearch(search)}`; - export const getRuleDetailsUrl = (detailName: string, search?: string) => `/id/${detailName}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 5020e910dfaa6..3c2e103c0dfd3 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -83,7 +83,9 @@ const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute spyState != null && spyState.pageName === SecurityPageName.administration; const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState => - spyState != null && spyState.pageName === SecurityPageName.rules; + spyState != null && + (spyState.pageName === SecurityPageName.rules || + spyState.pageName === SecurityPageName.rulesCreate); // eslint-disable-next-line complexity export const getBreadcrumbsForRoute = ( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 4430c8f030122..71b6852943ebf 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -30,6 +30,7 @@ import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/mod export const isDetectionsPages = (pageName: string) => pageName === SecurityPageName.alerts || pageName === SecurityPageName.rules || + pageName === SecurityPageName.rulesCreate || pageName === SecurityPageName.exceptions; export const decodeRisonUrlState = (value: string | undefined): T | null => { @@ -103,7 +104,7 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'network'; } else if (pageName === SecurityPageName.alerts) { return 'alerts'; - } else if (pageName === SecurityPageName.rules) { + } else if (pageName === SecurityPageName.rules || pageName === SecurityPageName.rulesCreate) { return 'rules'; } else if (pageName === SecurityPageName.exceptions) { return 'exceptions'; diff --git a/x-pack/plugins/security_solution/public/common/links/app_links.ts b/x-pack/plugins/security_solution/public/common/links/app_links.ts index 4a972bd5deb1f..1a78444012334 100644 --- a/x-pack/plugins/security_solution/public/common/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/common/links/app_links.ts @@ -33,6 +33,8 @@ export const appLinks: Readonly = Object.freeze([ }), ], links: [hostsLinks, networkLinks, usersLinks], + skipUrlState: true, + hideTimeline: true, }, timelinesLinks, getCasesLinkItems(), diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.ts b/x-pack/plugins/security_solution/public/common/links/links.test.ts index b86b05f48607d..b68ae3d863de3 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.test.ts @@ -98,9 +98,11 @@ const threatHuntingLinkInfo = { features: ['siem.show'], globalNavEnabled: false, globalSearchKeywords: ['Threat hunting'], - id: 'threat-hunting', + id: 'threat_hunting', path: '/threat_hunting', title: 'Threat Hunting', + hideTimeline: true, + skipUrlState: true, }; const hostsLinkInfo = { 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 af9357a122a1e..57965bdeba0c0 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -155,7 +155,6 @@ const getNormalizedLinks = ( * Normalized indexed version of the global `links` array, referencing the parent by id, instead of having nested links children */ const normalizedLinks: Readonly = Object.freeze(getNormalizedLinks(appLinks)); - /** * Returns the `NormalizedLink` from a link id parameter. * The object reference is frozen to make sure it is not mutated by the caller. @@ -193,3 +192,7 @@ export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => { export const needsUrlState = (id: SecurityPageName): boolean => { return !getNormalizedLink(id).skipUrlState; }; + +export const getLinksWithHiddenTimeline = (): LinkInfo[] => { + return Object.values(normalizedLinks).filter((link) => link.hideTimeline); +}; diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index 320c38d1d229b..bfa87851306ff 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -58,6 +58,7 @@ export interface LinkItem { links?: LinkItem[]; path: string; skipUrlState?: boolean; // defaults to false + hideTimeline?: boolean; // defaults to false title: string; } diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index 18e4af5886064..33a9f3a37a42f 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -17,40 +17,96 @@ jest.mock('react-router-dom', () => { }; }); +const mockedUseIsGroupedNavigationEnabled = jest.fn(); + +jest.mock('../../components/navigation/helpers', () => ({ + useIsGroupedNavigationEnabled: () => mockedUseIsGroupedNavigationEnabled(), +})); + describe('use show timeline', () => { - it('shows timeline for routes on default', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); - await waitForNextUpdate(); - const showTimeline = result.current; - expect(showTimeline).toEqual([true]); + describe('useIsGroupedNavigationEnabled false', () => { + beforeAll(() => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); }); - }); - it('hides timeline for blacklist routes', async () => { - mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); - await waitForNextUpdate(); - const showTimeline = result.current; - expect(showTimeline).toEqual([false]); + + it('shows timeline for routes on default', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([true]); + }); }); - }); - it('shows timeline for partial blacklist routes', async () => { - mockUseLocation.mockReturnValueOnce({ pathname: '/rules' }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); - await waitForNextUpdate(); - const showTimeline = result.current; - expect(showTimeline).toEqual([true]); + + it('hides timeline for blacklist routes', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([false]); + }); + }); + it('shows timeline for partial blacklist routes', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/rules' }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([true]); + }); + }); + it('hides timeline for sub blacklist routes', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([false]); + }); }); }); - it('hides timeline for sub blacklist routes', async () => { - mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' }); - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); - await waitForNextUpdate(); - const showTimeline = result.current; - expect(showTimeline).toEqual([false]); + + describe('useIsGroupedNavigationEnabled true', () => { + beforeAll(() => { + mockedUseIsGroupedNavigationEnabled.mockReturnValue(true); + }); + + it('shows timeline for routes on default', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([true]); + }); + }); + + it('hides timeline for blacklist routes', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([false]); + }); + }); + it('shows timeline for partial blacklist routes', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/rules' }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([true]); + }); + }); + it('hides timeline for sub blacklist routes', async () => { + mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' }); + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useShowTimeline()); + await waitForNextUpdate(); + const showTimeline = result.current; + expect(showTimeline).toEqual([false]); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx index 3378b13f8cb73..bb9eb075d735f 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx @@ -8,7 +8,10 @@ import { useState, useEffect } from 'react'; import { matchPath, useLocation } from 'react-router-dom'; -const HIDDEN_TIMELINE_ROUTES: readonly string[] = [ +import { getLinksWithHiddenTimeline } from '../../links'; +import { useIsGroupedNavigationEnabled } from '../../components/navigation/helpers'; + +const DEPRECATED_HIDDEN_TIMELINE_ROUTES: readonly string[] = [ `/cases/configure`, '/administration', '/rules/create', @@ -18,17 +21,27 @@ const HIDDEN_TIMELINE_ROUTES: readonly string[] = [ '/manage', ]; -const isHiddenTimelinePath = (currentPath: string): boolean => { - return !!HIDDEN_TIMELINE_ROUTES.find((route) => matchPath(currentPath, route)); +const isTimelineHidden = (currentPath: string, isGroupedNavigationEnabled: boolean): boolean => { + const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path); + + const hiddenTimelineRoutes = isGroupedNavigationEnabled + ? groupLinksWithHiddenTimelinePaths + : DEPRECATED_HIDDEN_TIMELINE_ROUTES; + + return !!hiddenTimelineRoutes.find((route) => matchPath(currentPath, route)); }; export const useShowTimeline = () => { + const isGroupedNavigationEnabled = useIsGroupedNavigationEnabled(); const { pathname } = useLocation(); - const [showTimeline, setShowTimeline] = useState(!isHiddenTimelinePath(pathname)); + + const [showTimeline, setShowTimeline] = useState( + !isTimelineHidden(pathname, isGroupedNavigationEnabled) + ); useEffect(() => { - setShowTimeline(!isHiddenTimelinePath(pathname)); - }, [pathname]); + setShowTimeline(!isTimelineHidden(pathname, isGroupedNavigationEnabled)); + }, [pathname, isGroupedNavigationEnabled]); return [showTimeline]; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index 0595fd96d1377..8228dc4e22274 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -161,9 +161,9 @@ describe('LoadPrebuiltRulesAndTemplatesButton', () => { await waitFor(() => { wrapper.update(); - expect( - wrapper.find('[data-test-subj="load-prebuilt-rules"] button').props().disabled - ).toEqual(true); + expect(wrapper.find('button[data-test-subj="load-prebuilt-rules"]').props().disabled).toEqual( + true + ); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index 281ef8c0f62ac..2d7551f1634c6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -9,14 +9,11 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { memo, useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { getCreateRuleUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import * as i18n from './translations'; -import { LinkButton } from '../../../../common/components/links'; +import { SecuritySolutionLinkButton } from '../../../../common/components/links'; import { SecurityPageName } from '../../../../app/types'; -import { useFormatUrl } from '../../../../common/components/link_to'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; import { useUserData } from '../../user_info'; -import { useNavigateTo } from '../../../../common/lib/kibana/hooks'; const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ @@ -38,16 +35,6 @@ const PrePackagedRulesPromptComponent: React.FC = ( const handlePreBuiltCreation = useCallback(() => { createPrePackagedRules(); }, [createPrePackagedRules]); - const { formatUrl } = useFormatUrl(SecurityPageName.rules); - const { navigateTo } = useNavigateTo(); - - const goToCreateRule = useCallback( - (ev) => { - ev.preventDefault(); - navigateTo({ deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl() }); - }, - [navigateTo] - ); const [{ isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite }] = useUserData(); @@ -80,14 +67,13 @@ const PrePackagedRulesPromptComponent: React.FC = ( {loadPrebuiltRulesAndTemplatesButton} - {i18n.CREATE_RULE_ACTION} - + } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index c7043f3725fcf..c37cba0e2b57f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -439,7 +439,7 @@ const CreateRulePageComponent: React.FC = () => { - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 05867a9830ad1..93d0e73c3017f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -33,7 +33,25 @@ jest.mock('../../../containers/detection_engine/rules/use_find_rules_query'); jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); -jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana', () => { + const actual = jest.requireActual('../../../../common/lib/kibana'); + return { + ...actual, + + useKibana: () => ({ + services: { + ...actual.useKibana().services, + application: { + navigateToApp: jest.fn(), + }, + }, + }), + useNavigation: () => ({ + navigateTo: jest.fn(), + }), + }; +}); + jest.mock('../../../../common/components/toasters', () => { const actual = jest.requireActual('../../../../common/components/toasters'); return { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 10d82bd4ba075..9281dbde77c2a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -10,10 +10,7 @@ import React, { useCallback, useMemo } from 'react'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config'; -import { - getDetectionEngineUrl, - getCreateRuleUrl, -} from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { getDetectionEngineUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; @@ -30,8 +27,7 @@ import { } from './helpers'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../app/types'; -import { LinkButton } from '../../../../common/components/links'; -import { useFormatUrl } from '../../../../common/components/link_to'; +import { SecuritySolutionLinkButton } from '../../../../common/components/links'; import { NeedAdminForUpdateRulesCallOut } from '../../../components/callouts/need_admin_for_update_callout'; import { MlJobCompatibilityCallout } from '../../../components/callouts/ml_job_compatibility_callout'; import { MissingPrivilegesCallOut } from '../../../components/callouts/missing_privileges_callout'; @@ -96,7 +92,6 @@ const RulesPageComponent: React.FC = () => { timelinesNotInstalled, timelinesNotUpdated ); - const { formatUrl } = useFormatUrl(SecurityPageName.rules); const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { @@ -113,14 +108,6 @@ const RulesPageComponent: React.FC = () => { } }, [refetchPrePackagedRulesStatus]); - const goToNewRule = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl() }); - }, - [navigateToApp] - ); - const loadPrebuiltRulesAndTemplatesButton = useMemo( () => getLoadPrebuiltRulesAndTemplatesButton({ @@ -212,16 +199,15 @@ const RulesPageComponent: React.FC = () => { - {i18n.ADD_NEW_RULE} - + diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 54c0b3f0d8dd2..ee60274cbb83d 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -12,14 +12,16 @@ import { EVENT_FILTERS_PATH, EXCEPTIONS_PATH, HOST_ISOLATION_EXCEPTIONS_PATH, - MANAGEMENT_PATH, + MANAGE_PATH, POLICIES_PATH, + RULES_CREATE_PATH, RULES_PATH, SecurityPageName, TRUSTED_APPS_PATH, } from '../../common/constants'; import { BLOCKLIST, + CREATE_NEW_RULE, ENDPOINTS, EVENT_FILTERS, EXCEPTIONS, @@ -44,8 +46,9 @@ import { IconTrustedApplications } from './icons/trusted_applications'; export const links: LinkItem = { id: SecurityPageName.administration, title: MANAGE, - path: MANAGEMENT_PATH, + path: MANAGE_PATH, skipUrlState: true, + hideTimeline: true, globalNavEnabled: false, features: [FEATURE.general], globalSearchKeywords: [ @@ -71,6 +74,16 @@ export const links: LinkItem = { }), ], globalSearchEnabled: true, + links: [ + { + id: SecurityPageName.rulesCreate, + title: CREATE_NEW_RULE, + path: RULES_CREATE_PATH, + globalNavEnabled: false, + skipUrlState: true, + hideTimeline: true, + }, + ], }, { id: SecurityPageName.exceptions, @@ -99,6 +112,7 @@ export const links: LinkItem = { globalNavOrder: 9006, path: ENDPOINTS_PATH, skipUrlState: true, + hideTimeline: true, }, { id: SecurityPageName.policies, @@ -110,6 +124,7 @@ export const links: LinkItem = { landingIcon: IconEndpointPolicies, path: POLICIES_PATH, skipUrlState: true, + hideTimeline: true, experimentalKey: 'policyListEnabled', }, { @@ -125,6 +140,7 @@ export const links: LinkItem = { landingIcon: IconTrustedApplications, path: TRUSTED_APPS_PATH, skipUrlState: true, + hideTimeline: true, }, { id: SecurityPageName.eventFilters, @@ -135,6 +151,7 @@ export const links: LinkItem = { landingIcon: IconEventFilters, path: EVENT_FILTERS_PATH, skipUrlState: true, + hideTimeline: true, }, { id: SecurityPageName.hostIsolationExceptions, @@ -145,6 +162,7 @@ export const links: LinkItem = { landingIcon: IconHostIsolation, path: HOST_ISOLATION_EXCEPTIONS_PATH, skipUrlState: true, + hideTimeline: true, }, { id: SecurityPageName.blocklist, @@ -155,6 +173,7 @@ export const links: LinkItem = { landingIcon: IconBlocklist, path: BLOCKLIST_PATH, skipUrlState: true, + hideTimeline: true, }, ], }; diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index d09c23a6cfc62..9fd06b523347f 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -48,6 +48,7 @@ export const gettingStartedLinks: LinkItem = { }), ], skipUrlState: true, + hideTimeline: true, }; export const detectionResponseLinks: LinkItem = { @@ -81,4 +82,6 @@ export const dashboardsLandingLinks: LinkItem = { }), ], links: [overviewLinks, detectionResponseLinks], + skipUrlState: true, + hideTimeline: true, }; 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 aac8d2e40f650..68051c047f230 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -12561,6 +12561,12 @@ "_meta": { "description": "The session cleanup interval that is configured, in minutes (0 if disabled)." } + }, + "anonymousCredentialType": { + "type": "keyword", + "_meta": { + "description": "The credential type that is configured for the anonymous authentication provider." + } } } }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx index 46b7fed8e14d4..e17721930858d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_dropdown_sandbox.tsx @@ -10,33 +10,33 @@ import { getRuleStatusDropdownLazy } from '../../../common/get_rule_status_dropd export const RuleStatusDropdownSandbox: React.FC<{}> = () => { const [enabled, setEnabled] = useState(true); - const [snoozeEndTime, setSnoozeEndTime] = useState(null); + const [isSnoozedUntil, setIsSnoozedUntil] = useState(null); const [muteAll, setMuteAll] = useState(false); return getRuleStatusDropdownLazy({ rule: { enabled, - snoozeEndTime, + isSnoozedUntil, muteAll, }, enableRule: async () => { setEnabled(true); setMuteAll(false); - setSnoozeEndTime(null); + setIsSnoozedUntil(null); }, disableRule: async () => setEnabled(false), snoozeRule: async (time) => { if (time === -1) { - setSnoozeEndTime(null); + setIsSnoozedUntil(null); setMuteAll(true); } else { - setSnoozeEndTime(new Date(time)); + setIsSnoozedUntil(new Date(time)); setMuteAll(false); } }, unsnoozeRule: async () => { setMuteAll(false); - setSnoozeEndTime(null); + setIsSnoozedUntil(null); }, onRuleChanged: () => {}, isEditable: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index 5377e4269f46e..104f0507aef8e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -243,7 +243,7 @@ describe('loadRuleAggregations', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "filter": "alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", "search": undefined, "search_fields": undefined, }, @@ -262,7 +262,7 @@ describe('loadRuleAggregations', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", "search": undefined, "search_fields": undefined, }, @@ -281,7 +281,7 @@ describe('loadRuleAggregations', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", "search": undefined, "search_fields": undefined, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts index 67838f4f84881..5648aa30820c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/common_transformations.ts @@ -43,7 +43,8 @@ export const transformRule: RewriteRequestCase = ({ scheduled_task_id: scheduledTaskId, execution_status: executionStatus, actions: actions, - snooze_end_time: snoozeEndTime, + snooze_schedule: snoozeSchedule, + is_snoozed_until: isSnoozedUntil, ...rest }: any) => ({ ruleTypeId, @@ -55,12 +56,13 @@ export const transformRule: RewriteRequestCase = ({ notifyWhen, muteAll, mutedInstanceIds, - snoozeEndTime, + snoozeSchedule, executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined, actions: actions ? actions.map((action: AsApiContract) => transformAction(action)) : [], scheduledTaskId, + isSnoozedUntil, ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index f67a27ef5409c..8d744c84d6f77 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -46,7 +46,7 @@ describe('mapFiltersToKql', () => { ruleStatusesFilter: ['enabled'], }) ).toEqual([ - 'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + 'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', ]); expect( @@ -54,21 +54,21 @@ describe('mapFiltersToKql', () => { ruleStatusesFilter: ['disabled'], }) ).toEqual([ - 'alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + 'alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', ]); expect( mapFiltersToKql({ ruleStatusesFilter: ['snoozed'], }) - ).toEqual(['(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)']); + ).toEqual(['(alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)']); expect( mapFiltersToKql({ ruleStatusesFilter: ['enabled', 'snoozed'], }) ).toEqual([ - 'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + 'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', ]); expect( @@ -76,7 +76,7 @@ describe('mapFiltersToKql', () => { ruleStatusesFilter: ['disabled', 'snoozed'], }) ).toEqual([ - 'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + 'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', ]); expect( @@ -84,7 +84,7 @@ describe('mapFiltersToKql', () => { ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'], }) ).toEqual([ - 'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + 'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', ]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index ff2a49e3a5e45..6629024e3eb11 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -57,7 +57,7 @@ export const mapFiltersToKql = ({ if (ruleStatusesFilter && ruleStatusesFilter.length) { const enablementFilter = getEnablementFilter(ruleStatusesFilter); - const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)`; + const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)`; const hasEnablement = ruleStatusesFilter.includes('enabled') || ruleStatusesFilter.includes('disabled'); const hasSnoozed = ruleStatusesFilter.includes('snoozed'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index 2a20c9d9469f5..e06ee24464d78 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -266,7 +266,7 @@ describe('loadRules', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", "page": 1, "per_page": 10, "search": undefined, @@ -295,7 +295,7 @@ describe('loadRules', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "filter": "alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", "page": 1, "per_page": 10, "search": undefined, @@ -324,7 +324,7 @@ describe('loadRules', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "filter": "alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", "page": 1, "per_page": 10, "search": undefined, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx index 15086518124b4..b2ea5e9a78aae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { RuleStatusDropdown, ComponentOpts } from './rule_status_dropdown'; const NOW_STRING = '2020-03-01T00:00:00.000Z'; -const SNOOZE_END_TIME = new Date('2020-03-04T00:00:00.000Z'); +const SNOOZE_UNTIL = new Date('2020-03-04T00:00:00.000Z'); describe('RuleStatusDropdown', () => { const enableRule = jest.fn(); @@ -51,7 +51,7 @@ describe('RuleStatusDropdown', () => { notifyWhen: null, index: 0, updatedAt: new Date('2020-08-20T19:23:38Z'), - snoozeEndTime: null, + snoozeSchedule: [], } as ComponentOpts['rule'], onRuleChanged: jest.fn(), }; @@ -86,7 +86,7 @@ describe('RuleStatusDropdown', () => { const wrapper = mountWithIntl( ); expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe('Snoozed'); @@ -108,7 +108,7 @@ describe('RuleStatusDropdown', () => { test('renders status control as disabled when rule is snoozed but also disabled', () => { const wrapper = mountWithIntl( ); expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe( @@ -121,7 +121,7 @@ describe('RuleStatusDropdown', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx index 90a42bd4fe21c..7c6a71e893f96 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_dropdown.tsx @@ -36,7 +36,7 @@ import { Rule } from '../../../../types'; type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M'; const SNOOZE_END_TIME_FORMAT = 'LL @ LT'; -type DropdownRuleRecord = Pick; +type DropdownRuleRecord = Pick; export interface ComponentOpts { rule: DropdownRuleRecord; @@ -74,6 +74,11 @@ const usePreviousSnoozeInterval: (p?: string | null) => [string | null, (n: stri return [previousSnoozeInterval, storeAndSetPreviousSnoozeInterval]; }; +const isRuleSnoozed = (rule: { isSnoozedUntil?: Date | null; muteAll: boolean }) => + Boolean( + (rule.isSnoozedUntil && new Date(rule.isSnoozedUntil).getTime() > Date.now()) || rule.muteAll + ); + export const RuleStatusDropdown: React.FunctionComponent = ({ rule, onRuleChanged, @@ -158,11 +163,13 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ isEnabled && isSnoozed ? ( - {rule.muteAll ? INDEFINITELY : moment(rule.snoozeEndTime).fromNow(true)} + {rule.muteAll ? INDEFINITELY : moment(new Date(rule.isSnoozedUntil!)).fromNow(true)} ) : null; @@ -215,7 +222,7 @@ export const RuleStatusDropdown: React.FunctionComponent = ({ onChangeSnooze={onChangeSnooze} isEnabled={isEnabled} isSnoozed={isSnoozed} - snoozeEndTime={rule.snoozeEndTime} + snoozeEndTime={rule.isSnoozedUntil} previousSnoozeInterval={previousSnoozeInterval} /> @@ -476,15 +483,6 @@ const SnoozePanel: React.FunctionComponent = ({ ); }; -const isRuleSnoozed = (rule: DropdownRuleRecord) => { - const { snoozeEndTime, muteAll } = rule; - if (muteAll) return true; - if (!snoozeEndTime) { - return false; - } - return moment(Date.now()).isBefore(snoozeEndTime); -}; - const futureTimeToInterval = (time?: Date | null) => { if (!time) return; const relativeTime = moment(time).locale('en').fromNow(true); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index e601c6ee15ec7..2d3829f42a678 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts @@ -115,6 +115,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { created_by: user.username, schedule: { interval: '1m' }, scheduled_task_id: response.body.scheduled_task_id, + snooze_schedule: response.body.snooze_schedule, created_at: response.body.created_at, updated_at: response.body.updated_at, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 20a5e82d303fe..177e51ab78eea 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -73,6 +73,7 @@ const findTestUtils = ( params: {}, created_by: 'elastic', scheduled_task_id: match.scheduled_task_id, + snooze_schedule: match.snooze_schedule, created_at: match.created_at, updated_at: match.updated_at, throttle: '1m', @@ -82,9 +83,7 @@ const findTestUtils = ( mute_all: false, muted_alert_ids: [], execution_status: match.execution_status, - ...(describeType === 'internal' - ? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time } - : {}), + ...(describeType === 'internal' ? { monitoring: match.monitoring } : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); @@ -283,9 +282,8 @@ const findTestUtils = ( created_at: match.created_at, updated_at: match.updated_at, execution_status: match.execution_status, - ...(describeType === 'internal' - ? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time } - : {}), + snooze_schedule: match.snooze_schedule, + ...(describeType === 'internal' ? { monitoring: match.monitoring } : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts index 48559aa35ac3c..c2c94af19b209 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts @@ -72,6 +72,7 @@ const getTestUtils = ( params: {}, created_by: 'elastic', scheduled_task_id: response.body.scheduled_task_id, + snooze_schedule: response.body.snooze_schedule, updated_at: response.body.updated_at, created_at: response.body.created_at, throttle: '1m', @@ -84,7 +85,6 @@ const getTestUtils = ( ...(describeType === 'internal' ? { monitoring: response.body.monitoring, - snooze_end_time: response.body.snooze_end_time, } : {}), }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts index 5a4c792463b62..f0ce5962de368 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts @@ -99,7 +99,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(true); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -156,7 +156,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(true); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -224,7 +224,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(true); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -292,7 +292,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(true); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts index 553e090498f00..0ca1ce4bf1eb7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts @@ -97,12 +97,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); + const now = Date.now(); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + // Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true); + expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be( + true + ); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -156,12 +163,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); + const now = Date.now(); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + // Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true); + expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be( + true + ); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -226,12 +240,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); + const now = Date.now(); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + // Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true); + expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be( + true + ); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -296,12 +317,19 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); + const now = Date.now(); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + // Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true); + expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be( + true + ); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -383,7 +411,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.snooze_schedule).to.eql([]); expect(updatedAlert.mute_all).to.eql(true); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts index dde198f54f771..9c918b3225f9e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts @@ -104,7 +104,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(false); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -166,7 +166,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(false); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -239,7 +239,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(false); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, @@ -312,7 +312,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex .auth(user.username, user.password) .expect(200); expect(updatedAlert.mute_all).to.eql(false); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts index c868654235c21..8b6a8aa2c6c45 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts @@ -98,7 +98,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.snooze_schedule).to.eql(null); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -155,7 +155,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.snooze_schedule).to.eql(null); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -223,7 +223,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.snooze_schedule).to.eql(null); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -291,7 +291,7 @@ export default function createUnsnoozeRuleTests({ getService }: FtrProviderConte .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.snooze_schedule).to.eql(null); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts index c49fa62c606b6..d28b81f479b11 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts @@ -129,6 +129,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }, ], scheduled_task_id: createdAlert.scheduled_task_id, + snooze_schedule: createdAlert.snooze_schedule, created_at: response.body.created_at, updated_at: response.body.updated_at, execution_status: response.body.execution_status, @@ -213,6 +214,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, + snooze_schedule: createdAlert.snooze_schedule, created_at: response.body.created_at, updated_at: response.body.updated_at, execution_status: response.body.execution_status, @@ -308,6 +310,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, + snooze_schedule: createdAlert.snooze_schedule, created_at: response.body.created_at, updated_at: response.body.updated_at, execution_status: response.body.execution_status, @@ -403,6 +406,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, + snooze_schedule: createdAlert.snooze_schedule, created_at: response.body.created_at, updated_at: response.body.updated_at, execution_status: response.body.execution_status, @@ -496,6 +500,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, + snooze_schedule: createdAlert.snooze_schedule, created_at: response.body.created_at, updated_at: response.body.updated_at, execution_status: response.body.execution_status, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 143d845d074c4..a33f7fc5a1a2c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -85,6 +85,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { created_by: null, schedule: { interval: '1m' }, scheduled_task_id: response.body.scheduled_task_id, + snooze_schedule: response.body.snooze_schedule, updated_by: null, api_key_owner: null, throttle: '1m', @@ -180,6 +181,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { created_by: null, schedule: { interval: '1m' }, scheduled_task_id: response.body.scheduled_task_id, + snooze_schedule: response.body.snooze_schedule, updated_by: null, api_key_owner: null, throttle: '1m', @@ -475,6 +477,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { createdBy: null, schedule: { interval: '1m' }, scheduledTaskId: response.body.scheduledTaskId, + snoozeSchedule: response.body.snoozeSchedule, updatedBy: null, apiKeyOwner: null, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index a1b0f5c7eeb14..021a2be1ebb5d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -64,6 +64,7 @@ const findTestUtils = ( created_by: null, api_key_owner: null, scheduled_task_id: match.scheduled_task_id, + snooze_schedule: match.snooze_schedule, updated_by: null, throttle: '1m', notify_when: 'onThrottleInterval', @@ -72,9 +73,7 @@ const findTestUtils = ( created_at: match.created_at, updated_at: match.updated_at, execution_status: match.execution_status, - ...(describeType === 'internal' - ? { monitoring: match.monitoring, snooze_end_time: match.snooze_end_time } - : {}), + ...(describeType === 'internal' ? { monitoring: match.monitoring } : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); @@ -296,6 +295,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { createdBy: null, apiKeyOwner: null, scheduledTaskId: match.scheduledTaskId, + snoozeSchedule: match.snoozeSchedule, updatedBy: null, throttle: '1m', notifyWhen: 'onThrottleInterval', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 58c68def04372..ee993c425fa38 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -45,6 +45,7 @@ const getTestUtils = ( params: {}, created_by: null, scheduled_task_id: response.body.scheduled_task_id, + snooze_schedule: response.body.snooze_schedule, updated_by: null, api_key_owner: null, throttle: '1m', @@ -55,7 +56,7 @@ const getTestUtils = ( updated_at: response.body.updated_at, execution_status: response.body.execution_status, ...(describeType === 'internal' - ? { monitoring: response.body.monitoring, snooze_end_time: response.body.snooze_end_time } + ? { monitoring: response.body.monitoring, snooze_schedule: response.body.snooze_schedule } : {}), }); expect(Date.parse(response.body.created_at)).to.be.greaterThan(0); @@ -136,6 +137,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { params: {}, createdBy: null, scheduledTaskId: response.body.scheduledTaskId, + snoozeSchedule: response.body.snoozeSchedule, updatedBy: null, apiKeyOwner: null, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts index 53517b191bab6..a56b95ed09219 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts @@ -41,7 +41,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mute_all).to.eql(true); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ supertest: supertestWithoutAuth, @@ -70,7 +70,7 @@ export default function createMuteTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mute_all).to.eql(true); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts index 5be5b59a15248..80cfa5a105467 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/snooze.ts @@ -70,11 +70,16 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); + const now = Date.now(); const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) .set('kbn-xsrf', 'foo') .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(FUTURE_SNOOZE_TIME); + expect(updatedAlert.snooze_schedule.length).to.eql(1); + // Due to latency, test to make sure the returned rRule.dtstart is within 10 seconds of the current time + const { rRule, duration } = updatedAlert.snooze_schedule[0]; + expect(Math.abs(Date.parse(rRule.dtstart) - now) < 10000).to.be(true); + expect(Math.abs(duration - (Date.parse(FUTURE_SNOOZE_TIME) - now)) < 10000).to.be(true); expect(updatedAlert.mute_all).to.eql(false); // Ensure AAD isn't broken await checkAAD({ @@ -126,7 +131,7 @@ export default function createSnoozeRuleTests({ getService }: FtrProviderContext .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}`) .set('kbn-xsrf', 'foo') .expect(200); - expect(updatedAlert.snooze_end_time).to.eql(null); + expect(updatedAlert.snooze_schedule).to.eql([]); expect(updatedAlert.mute_all).to.eql(true); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts index 782df6d86d542..62ff63052f841 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts @@ -42,7 +42,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mute_all).to.eql(false); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ @@ -76,7 +76,7 @@ export default function createUnmuteTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .expect(200); expect(updatedAlert.mute_all).to.eql(false); - expect(updatedAlert.snooze_end_time).to.eql(undefined); + expect(updatedAlert.snooze_schedule.length).to.eql(0); // Ensure AAD isn't broken await checkAAD({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index c5a9c93d45e81..c431654f0fd20 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -60,6 +60,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { muted_alert_ids: [], notify_when: 'onThrottleInterval', scheduled_task_id: createdAlert.scheduled_task_id, + snooze_schedule: createdAlert.snooze_schedule, created_at: response.body.created_at, updated_at: response.body.updated_at, execution_status: response.body.execution_status, @@ -160,6 +161,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { mutedInstanceIds: [], notifyWhen: 'onThrottleInterval', scheduledTaskId: createdAlert.scheduled_task_id, + snoozeSchedule: createdAlert.snooze_schedule, createdAt: response.body.createdAt, updatedAt: response.body.updatedAt, executionStatus: response.body.executionStatus, diff --git a/x-pack/test/api_integration/apis/maps/get_grid_tile.js b/x-pack/test/api_integration/apis/maps/get_grid_tile.js index 36e4d678093a7..46fdda09ec476 100644 --- a/x-pack/test/api_integration/apis/maps/get_grid_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_grid_tile.js @@ -12,8 +12,7 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertest = getService('supertest'); - // Failing: See https://github.com/elastic/kibana/issues/132372 - describe.skip('getGridTile', () => { + describe('getGridTile', () => { const URL = `/api/maps/mvt/getGridTile/3/2/3.pbf\ ?geometryFieldName=geo.coordinates\ &index=logstash-*\ @@ -110,9 +109,9 @@ export default function ({ getService }) { expect(gridFeature.loadGeometry()).to.eql([ [ { x: 80, y: 672 }, - { x: 96, y: 672 }, - { x: 96, y: 656 }, { x: 80, y: 656 }, + { x: 96, y: 656 }, + { x: 96, y: 672 }, { x: 80, y: 672 }, ], ]); @@ -143,11 +142,11 @@ export default function ({ getService }) { expect(gridFeature.loadGeometry()).to.eql([ [ { x: 102, y: 669 }, - { x: 99, y: 659 }, - { x: 89, y: 657 }, - { x: 83, y: 664 }, - { x: 86, y: 674 }, { x: 96, y: 676 }, + { x: 86, y: 674 }, + { x: 83, y: 664 }, + { x: 89, y: 657 }, + { x: 99, y: 659 }, { x: 102, y: 669 }, ], ]); @@ -186,9 +185,9 @@ export default function ({ getService }) { expect(metadataFeature.loadGeometry()).to.eql([ [ { x: 0, y: 4096 }, - { x: 4096, y: 4096 }, - { x: 4096, y: 0 }, { x: 0, y: 0 }, + { x: 4096, y: 0 }, + { x: 4096, y: 4096 }, { x: 0, y: 4096 }, ], ]); diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index d8754f8c0b0c6..09b8bf1d8b862 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -21,8 +21,7 @@ function findFeature(layer, callbackFn) { export default function ({ getService }) { const supertest = getService('supertest'); - // Failing: See https://github.com/elastic/kibana/issues/132368 - describe.skip('getTile', () => { + describe('getTile', () => { it('should return ES vector tile containing documents and metadata', async () => { const resp = await supertest .get( @@ -78,9 +77,9 @@ export default function ({ getService }) { expect(metadataFeature.loadGeometry()).to.eql([ [ { x: 44, y: 2382 }, - { x: 550, y: 2382 }, - { x: 550, y: 1913 }, { x: 44, y: 1913 }, + { x: 550, y: 1913 }, + { x: 550, y: 2382 }, { x: 44, y: 2382 }, ], ]); diff --git a/yarn.lock b/yarn.lock index 3f24988cc3401..68ca4003956fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7006,6 +7006,13 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/rrule@^2.2.9": + version "2.2.9" + resolved "https://registry.yarnpkg.com/@types/rrule/-/rrule-2.2.9.tgz#b25222b5057b9a9e6eea28ce9e94673a957c960f" + integrity sha512-OWTezBoGwsL2nn9SFbLbiTrAic1hpxAIRqeF8QDB84iW6KBEAHM6Oj9T2BEokgeIDgT1q73sfD0gI1S2yElSFA== + dependencies: + rrule "*" + "@types/seedrandom@>=2.0.0 <4.0.0": version "2.4.28" resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" @@ -19469,11 +19476,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" -luxon@^1.25.0: +luxon@^1.21.3, luxon@^1.25.0: version "1.28.0" resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== +luxon@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.3.2.tgz#5f2f3002b8c39b60a7b7ad24b2a85d90dc5db49c" + integrity sha512-MlAQQVMFhGk4WUA6gpfsy0QycnKP0+NlCBJRVRNPxxSIbjrCbQ65nrpJD3FVyJNZLuJ0uoqL57ye6BmDYgHaSw== + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -25234,6 +25246,24 @@ rollup@^0.25.8: minimist "^1.2.0" source-map-support "^0.3.2" +rrule@*: + version "2.6.9" + resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.6.9.tgz#8ee4ee261451e84852741f92ded769245580744a" + integrity sha512-PE4ErZDMfAcRnc1B35bZgPGS9mbn7Z9bKDgk6+XgrIwvBjeWk7JVEYsqKwHYTrDGzsHPtZTpaon8IyeKzAhj5w== + dependencies: + tslib "^1.10.0" + optionalDependencies: + luxon "^1.21.3" + +rrule@2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.6.4.tgz#7f4f31fda12bc7249bb176c891109a9bc448e035" + integrity sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA== + dependencies: + tslib "^1.10.0" + optionalDependencies: + luxon "^1.21.3" + rst-selector-parser@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"