From 7bd2e510c769e2b4f69d7dc7134921b535efc4fe Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Thu, 2 Feb 2023 12:12:48 +0100 Subject: [PATCH 01/35] [Infrastructure UI] Fix broken type on telemetry tracking (#150147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary [This PR](https://github.com/elastic/kibana/pull/149497) passed the CI and has been pending for review a couple of days. Meantime, a new change that updated that type was introduced on a different PR and the CI didn't run again on pending work. This change fixes the issue on the type checking. Co-authored-by: Marco Antonio Ghiani --- .../metrics/hosts/hooks/use_unified_search.ts | 20 ++++++++++++++----- .../hooks/use_unified_search_url_state.ts | 5 +++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts index 78e4f9bfeee4..898d384bcdc2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts @@ -15,9 +15,16 @@ import { telemetryTimeRangeFormatter } from '../../../../../common/formatters/te import type { InfraClientStartDeps } from '../../../../types'; import { useMetricsDataViewContext } from './use_data_view'; import { useSyncKibanaTimeFilterTime } from '../../../../hooks/use_kibana_timefilter_time'; -import { useHostsUrlState, INITIAL_DATE_RANGE, HostsState } from './use_unified_search_url_state'; - -const buildQuerySubmittedPayload = (hostState: HostsState) => { +import { + useHostsUrlState, + INITIAL_DATE_RANGE, + HostsState, + StringDateRangeTimestamp, +} from './use_unified_search_url_state'; + +const buildQuerySubmittedPayload = ( + hostState: HostsState & { dateRangeTimestamp: StringDateRangeTimestamp } +) => { const { panelFilters, filters, dateRangeTimestamp, query: queryObj } = hostState; return { @@ -77,8 +84,11 @@ export const useUnifiedSearch = () => { // Track telemetry event on query/filter/date changes useEffect(() => { - telemetry.reportHostsViewQuerySubmitted(buildQuerySubmittedPayload(state)); - }, [state, telemetry]); + const dateRangeTimestamp = getDateRangeAsTimestamp(); + telemetry.reportHostsViewQuerySubmitted( + buildQuerySubmittedPayload({ ...state, dateRangeTimestamp }) + ); + }, [getDateRangeAsTimestamp, state, telemetry]); const onSubmit = useCallback( (data?: { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts index 1a19f21626d8..41e476dbf12c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts @@ -143,6 +143,11 @@ const HostsStateRT = rt.type({ export type HostsState = rt.TypeOf; +export interface StringDateRangeTimestamp { + from: number; + to: number; +} + const SetQueryType = rt.partial(HostsStateRT.props); const encodeUrlState = HostsStateRT.encode; From bd97ad681cfcf9e1984ed349c12ce93d5fcc3ae0 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Thu, 2 Feb 2023 12:43:53 +0100 Subject: [PATCH 02/35] [Enterprise Search] Update copy for disabled crawler logs (#150011) ## Summary Updates copy for the Elastic Web Crawler in case of disabled crawler logs. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../crawl_details_summary.tsx | 26 ++++++++++++++----- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx index 43f0b1d57017..52f02b6833fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/crawl_details_flyout/crawl_details_summary.tsx @@ -14,12 +14,17 @@ import { EuiFlexItem, EuiHorizontalRule, EuiIconTip, + EuiLink, EuiPanel, EuiSpacer, EuiStat, EuiText, } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { docLinks } from '../../../../../shared/doc_links'; import { CrawlRequestStats } from '../../../../api/crawler/types'; @@ -236,13 +241,20 @@ export const CrawlDetailsSummary: React.FC = ({

- {i18n.translate( - 'xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage', - { - defaultMessage: - 'Enable Web Crawler logs in settings for more detailed crawl statistics.', - } - )} + + {i18n.translate( + 'xpack.enterpriseSearch.crawler.crawlDetailsSummary.configLink', + { defaultMessage: 'Enable web crawler logs' } + )} + + ), + }} + />

)} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4e4e22cefafe..8512d432ec93 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12117,7 +12117,6 @@ "xpack.enterpriseSearch.crawler.crawlDetailsSummary.avgResponseTimeLabel": "Réponse moy.", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.clientErrorsLabel": "Erreurs 4xx", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.durationTooltipTitle": "Durée", - "xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage": "Activer les journaux du robot d'indexation dans les paramètres pour obtenir des statistiques d'indexation plus détaillées.", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltip": "URL visitées et extraites pendant l'indexation.", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltipTitle": "Pages visitées", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle": "Pages", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2b089e8c055b..84bad048be87 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12104,7 +12104,6 @@ "xpack.enterpriseSearch.crawler.crawlDetailsSummary.avgResponseTimeLabel": "平均応答", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.clientErrorsLabel": "4xxエラー", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.durationTooltipTitle": "期間", - "xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage": "詳細なクロール統計情報については、設定でWebクローラーログを有効にします。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltip": "クロール中にアクセスされ抽出されたページ。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltipTitle": "アクセスされたページ", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle": "ページ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 849f629a69ee..b8dceab4af56 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12121,7 +12121,6 @@ "xpack.enterpriseSearch.crawler.crawlDetailsSummary.avgResponseTimeLabel": "平均响应", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.clientErrorsLabel": "4xx 错误", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.durationTooltipTitle": "持续时间", - "xpack.enterpriseSearch.crawler.crawlDetailsSummary.logsDisabledMessage": "在设置中启用网络爬虫日志以获取更详细的爬网统计信息。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltip": "在爬网期间访问并提取的 URL。", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesTooltipTitle": "访问的页面", "xpack.enterpriseSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle": "页面", From 4a1373c46fafe71be6334838413f71683ab263fa Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 2 Feb 2023 13:10:43 +0100 Subject: [PATCH 03/35] Improve security of links using target=_blank (#150049) --- .../src/errors/public_base_url.tsx | 6 +++++- .../source_field_section/source_field_section.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx b/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx index ec5f45930ce2..0d2e678963d8 100644 --- a/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/errors/public_base_url.tsx @@ -58,7 +58,11 @@ export const setupPublicBaseUrlConfigWarning = ({ configKey: server.publicBaseUrl, }} />{' '} - + {

- + Date: Thu, 2 Feb 2023 13:13:55 +0100 Subject: [PATCH 04/35] [APM] Add missing `IntlProvider` to unit test (#150137) Jest tests in APM are currently failing with: ``` Error: Uncaught [Error: [React Intl] Could not find required `intl` object. needs to exist in the component ancestry. Using default message as fallback.] ``` Adding the `IntlProvider` fixes the problem --- .../transaction_overview.test.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index e130802408d4..5b3118e527fa 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -23,6 +23,7 @@ import { } from '../../../utils/test_helpers'; import { fromQuery } from '../../shared/links/url_helpers'; import { TransactionOverview } from '.'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; const KibanaReactContext = createKibanaReactContext({ uiSettings: { get: () => true }, @@ -64,15 +65,17 @@ function setup({ jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); return renderWithTheme( - - - - - - - - - + + + + + + + + + + + ); } From 0f12bbce71e3432e28af6ead666ca57d7883affd Mon Sep 17 00:00:00 2001 From: Elastic Machine Date: Thu, 2 Feb 2023 23:30:11 +1030 Subject: [PATCH 05/35] [main] Sync bundled packages with Package Storage (#150130) Automated by https://internal-ci.elastic.co/job/package_storage/job/sync-bundled-packages-job/job/main/1602/ Co-authored-by: apmmachine --- fleet_packages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fleet_packages.json b/fleet_packages.json index 63195b19e220..fcb628f52610 100644 --- a/fleet_packages.json +++ b/fleet_packages.json @@ -25,7 +25,7 @@ }, { "name": "elastic_agent", - "version": "1.4.1" + "version": "1.5.0" }, { "name": "endpoint", From cb7cc4a4c8caf8f5c6086e17a804f797791cc395 Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Thu, 2 Feb 2023 08:32:30 -0500 Subject: [PATCH 06/35] [ResponseOps][Flapping] Update flapping code once the flapping lookback value is configurable (#149448) Resolves https://github.com/elastic/kibana/issues/145929 ## Summary Updates previous flapping tests to use the new flapping settings configs. Updates flapping logic to use flapping configs instead of hardcoded values. Calls the flapping api on every rule execution, and then passes in the flapping settings to the rule executors so they can be used by the rule registry. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### To verify I think it's helpful to hide the whitespace when reviewing this pr. - The flapping logic should remain the same, and all previous tests should pass. I only updated them to pass in the flapping settings. - Create rules, and set flapping settings in the ui and see the flapping behavior change for your rules. - Verify that the `x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts` run with the new flapping configs and output results we would expect --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/alerting/common/rules_settings.ts | 11 +- .../server/alert/create_alert_factory.ts | 5 +- .../legacy_alerts_client.test.ts | 14 ++- .../alerts_client/legacy_alerts_client.ts | 7 +- .../server/lib/flapping_utils.test.ts | 47 +++++-- .../alerting/server/lib/flapping_utils.ts | 49 +++++--- .../lib/get_alerts_for_notification.test.ts | 116 ++++++++++++++++++ .../server/lib/get_alerts_for_notification.ts | 53 ++++---- .../server/lib/process_alerts.test.ts | 59 ++++----- .../alerting/server/lib/process_alerts.ts | 50 ++++---- .../alerting/server/lib/set_flapping.test.ts | 84 +++++++++++-- .../alerting/server/lib/set_flapping.ts | 15 ++- x-pack/plugins/alerting/server/plugin.ts | 5 + .../server/rules_settings_client.mock.ts | 8 +- .../server/task_runner/task_runner.test.ts | 5 + .../server/task_runner/task_runner.ts | 4 + .../task_runner/task_runner_cancel.test.ts | 5 + .../task_runner/task_runner_factory.test.ts | 2 + .../server/task_runner/task_runner_factory.ts | 2 + x-pack/plugins/alerting/server/types.ts | 2 + .../server/routes/alerts/test_utils/index.ts | 2 + .../metric_threshold_executor.test.ts | 2 + .../lib/rules/slo_burn_rate/executor.test.ts | 6 + .../server/utils/create_lifecycle_executor.ts | 6 +- .../utils/create_lifecycle_rule_type.test.ts | 2 + .../utils/get_alerts_for_notification.test.ts | 43 ++++++- .../utils/get_alerts_for_notification.ts | 16 ++- .../get_updated_flapping_history.test.ts | 99 +++++++++++++-- .../utils/get_updated_flapping_history.ts | 56 +++++---- .../utils/rule_executor.test_helpers.ts | 2 + ...gacy_rules_notification_alert_type.test.ts | 2 + .../rule_preview/api/preview_rules/route.ts | 3 +- .../rule_types/es_query/rule_type.test.ts | 2 + .../index_threshold/rule_type.test.ts | 5 + .../common/lib/index.ts | 1 + .../common/lib/reset_rules_settings.ts | 19 +++ .../tests/alerting/get_flapping_settings.ts | 12 +- .../alerting/update_flapping_settings.ts | 11 +- .../tests/alerting/group1/event_log.ts | 82 ++++++++----- .../tests/trial/get_summarized_alerts.ts | 3 + 40 files changed, 706 insertions(+), 211 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts diff --git a/x-pack/plugins/alerting/common/rules_settings.ts b/x-pack/plugins/alerting/common/rules_settings.ts index 755becc8a982..743d5f4236aa 100644 --- a/x-pack/plugins/alerting/common/rules_settings.ts +++ b/x-pack/plugins/alerting/common/rules_settings.ts @@ -44,8 +44,13 @@ export const RULES_SETTINGS_SAVED_OBJECT_ID = 'rules-settings'; export const DEFAULT_LOOK_BACK_WINDOW = 20; export const DEFAULT_STATUS_CHANGE_THRESHOLD = 4; -export const DEFAULT_FLAPPING_SETTINGS = { +export const DEFAULT_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = { enabled: true, - lookBackWindow: 20, - statusChangeThreshold: 4, + lookBackWindow: DEFAULT_LOOK_BACK_WINDOW, + statusChangeThreshold: DEFAULT_STATUS_CHANGE_THRESHOLD, +}; + +export const DISABLE_FLAPPING_SETTINGS: RulesSettingsFlappingProperties = { + ...DEFAULT_FLAPPING_SETTINGS, + enabled: false, }; diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts index 99f4c3f2b5da..e0ea1b9d1fba 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -10,6 +10,7 @@ import { cloneDeep } from 'lodash'; import { AlertInstanceContext, AlertInstanceState } from '../types'; import { Alert, PublicAlert } from './alert'; import { processAlerts } from '../lib'; +import { DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; export interface AlertFactory< State extends AlertInstanceState, @@ -149,8 +150,8 @@ export function createAlertFactory< hasReachedAlertLimit, alertLimit: maxAlerts, autoRecoverAlerts, - // setFlapping is false, as we only want to use this function to get the recovered alerts - setFlapping: false, + // flappingSettings.enabled is false, as we only want to use this function to get the recovered alerts + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); return Object.keys(currentRecoveredAlerts ?? {}).map( (alertId: string) => currentRecoveredAlerts[alertId] diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts index dddfe857cfd4..e65d4e477e87 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts @@ -14,6 +14,7 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e import { ruleRunMetricsStoreMock } from '../lib/rule_run_metrics_store.mock'; import { getAlertsForNotification, processAlerts, setFlapping } from '../lib'; import { logAlerts } from '../task_runner/log_alerts'; +import { DEFAULT_FLAPPING_SETTINGS } from '../../common/rules_settings'; const scheduleActions = jest.fn(); const replaceState = jest.fn(() => ({ scheduleActions })); @@ -229,6 +230,7 @@ describe('Legacy Alerts Client', () => { ruleLabel: `ruleLogPrefix`, ruleRunMetricsStore, shouldLogAndScheduleActionsForAlerts: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(processAlerts).toHaveBeenCalledWith({ @@ -244,10 +246,15 @@ describe('Legacy Alerts Client', () => { hasReachedAlertLimit: false, alertLimit: 1000, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(setFlapping).toHaveBeenCalledWith( + { + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 4, + }, { '1': new Alert('1', testAlert1), '2': new Alert('2', testAlert2), @@ -256,6 +263,11 @@ describe('Legacy Alerts Client', () => { ); expect(getAlertsForNotification).toHaveBeenCalledWith( + { + enabled: true, + lookBackWindow: 20, + statusChangeThreshold: 4, + }, 'default', {}, { diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts index 4c32c1cbcc92..e935fcd9938d 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.ts @@ -29,6 +29,7 @@ import { RawAlertInstance, WithoutReservedActionGroups, } from '../types'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; interface ConstructorOpts { logger: Logger; @@ -111,11 +112,13 @@ export class LegacyAlertsClient< ruleLabel, ruleRunMetricsStore, shouldLogAndScheduleActionsForAlerts, + flappingSettings, }: { eventLogger: AlertingEventLogger; ruleLabel: string; shouldLogAndScheduleActionsForAlerts: boolean; ruleRunMetricsStore: RuleRunMetricsStore; + flappingSettings: RulesSettingsFlappingProperties; }) { const { newAlerts: processedAlertsNew, @@ -132,10 +135,11 @@ export class LegacyAlertsClient< this.options.ruleType.autoRecoverAlerts !== undefined ? this.options.ruleType.autoRecoverAlerts : true, - setFlapping: true, + flappingSettings, }); setFlapping( + flappingSettings, processedAlertsActive, processedAlertsRecovered ); @@ -147,6 +151,7 @@ export class LegacyAlertsClient< ); const alerts = getAlertsForNotification( + flappingSettings, this.options.ruleType.defaultActionGroupId, processedAlertsNew, processedAlertsActive, diff --git a/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts b/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts index ee5525634cf4..a2dbb7109ab6 100644 --- a/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts +++ b/x-pack/plugins/alerting/server/lib/flapping_utils.test.ts @@ -5,18 +5,23 @@ * 2.0. */ +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; import { atCapacity, updateFlappingHistory, isFlapping } from './flapping_utils'; describe('flapping utils', () => { describe('updateFlappingHistory function', () => { test('correctly updates flappingHistory', () => { - const flappingHistory = updateFlappingHistory([false, false], true); + const flappingHistory = updateFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + [false, false], + true + ); expect(flappingHistory).toEqual([false, false, true]); }); test('correctly updates flappingHistory while maintaining a fixed size', () => { const flappingHistory = new Array(20).fill(false); - const fh = updateFlappingHistory(flappingHistory, true); + const fh = updateFlappingHistory(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true); expect(fh.length).toEqual(20); const result = new Array(19).fill(false); expect(fh).toEqual(result.concat(true)); @@ -24,27 +29,36 @@ describe('flapping utils', () => { test('correctly updates flappingHistory while maintaining if array is larger than fixed size', () => { const flappingHistory = new Array(23).fill(false); - const fh = updateFlappingHistory(flappingHistory, true); + const fh = updateFlappingHistory(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true); expect(fh.length).toEqual(20); const result = new Array(19).fill(false); expect(fh).toEqual(result.concat(true)); }); + + test('does not update flappingHistory if flapping is disabled', () => { + const flappingHistory = updateFlappingHistory( + DISABLE_FLAPPING_SETTINGS, + [false, false], + true + ); + expect(flappingHistory).toEqual([false, false]); + }); }); describe('atCapacity and getCapacityDiff functions', () => { test('returns true if flappingHistory == set capacity', () => { const flappingHistory = new Array(20).fill(false); - expect(atCapacity(flappingHistory)).toEqual(true); + expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); test('returns true if flappingHistory > set capacity', () => { const flappingHistory = new Array(25).fill(false); - expect(atCapacity(flappingHistory)).toEqual(true); + expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); test('returns false if flappingHistory < set capacity', () => { const flappingHistory = new Array(15).fill(false); - expect(atCapacity(flappingHistory)).toEqual(false); + expect(atCapacity(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(false); }); }); @@ -52,39 +66,46 @@ describe('flapping utils', () => { describe('not currently flapping', () => { test('returns true if at capacity and flap count exceeds the threshold', () => { const flappingHistory = [true, true, true, true].concat(new Array(16).fill(false)); - expect(isFlapping(flappingHistory)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); test("returns false if at capacity and flap count doesn't exceed the threshold", () => { const flappingHistory = [true, true].concat(new Array(20).fill(false)); - expect(isFlapping(flappingHistory)).toEqual(false); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(false); }); test('returns true if not at capacity', () => { const flappingHistory = new Array(5).fill(true); - expect(isFlapping(flappingHistory)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory)).toEqual(true); }); }); describe('currently flapping', () => { test('returns true if at capacity and the flap count exceeds the threshold', () => { const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]); - expect(isFlapping(flappingHistory, true)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true); }); test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => { const flappingHistory = new Array(16).fill(false); - expect(isFlapping(flappingHistory, true)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true); }); test('returns true if not at capacity and the flap count exceeds the threshold', () => { const flappingHistory = new Array(10).fill(false).concat([true, true, true, true]); - expect(isFlapping(flappingHistory, true)).toEqual(true); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(true); }); test("returns false if at capacity and the flap count doesn't exceed the threshold", () => { const flappingHistory = new Array(20).fill(false); - expect(isFlapping(flappingHistory, true)).toEqual(false); + expect(isFlapping(DEFAULT_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(false); + }); + }); + + describe('flapping disabled', () => { + test('returns false if flapping is disabled', () => { + const flappingHistory = new Array(16).fill(false).concat([true, true, true, true]); + expect(isFlapping(DISABLE_FLAPPING_SETTINGS, flappingHistory, true)).toEqual(false); }); }); }); diff --git a/x-pack/plugins/alerting/server/lib/flapping_utils.ts b/x-pack/plugins/alerting/server/lib/flapping_utils.ts index 30e7059b2bc8..9c0f1c231f17 100644 --- a/x-pack/plugins/alerting/server/lib/flapping_utils.ts +++ b/x-pack/plugins/alerting/server/lib/flapping_utils.ts @@ -5,31 +5,46 @@ * 2.0. */ -const MAX_CAPACITY = 20; -export const MAX_FLAP_COUNT = 4; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; -export function updateFlappingHistory(flappingHistory: boolean[], state: boolean) { - const updatedFlappingHistory = flappingHistory.concat(state).slice(MAX_CAPACITY * -1); - return updatedFlappingHistory; +export function updateFlappingHistory( + flappingSettings: RulesSettingsFlappingProperties, + flappingHistory: boolean[], + state: boolean +) { + if (flappingSettings.enabled) { + const updatedFlappingHistory = flappingHistory + .concat(state) + .slice(flappingSettings.lookBackWindow * -1); + return updatedFlappingHistory; + } + return flappingHistory; } export function isFlapping( + flappingSettings: RulesSettingsFlappingProperties, flappingHistory: boolean[], isCurrentlyFlapping: boolean = false ): boolean { - const numStateChanges = flappingHistory.filter((f) => f).length; - if (isCurrentlyFlapping) { - // if an alert is currently flapping, - // it will return false if the flappingHistory array is at capacity and there are 0 state changes - // else it will return true - return !(atCapacity(flappingHistory) && numStateChanges === 0); - } else { - // if an alert is not currently flapping, - // it will return true if the number of state changes in flappingHistory array >= the max flapping count - return numStateChanges >= MAX_FLAP_COUNT; + if (flappingSettings.enabled) { + const numStateChanges = flappingHistory.filter((f) => f).length; + if (isCurrentlyFlapping) { + // if an alert is currently flapping, + // it will return false if the flappingHistory array is at capacity and there are 0 state changes + // else it will return true + return !(atCapacity(flappingSettings, flappingHistory) && numStateChanges === 0); + } else { + // if an alert is not currently flapping, + // it will return true if the number of state changes in flappingHistory array >= the flapping status change threshold + return numStateChanges >= flappingSettings.statusChangeThreshold; + } } + return false; } -export function atCapacity(flappingHistory: boolean[] = []): boolean { - return flappingHistory.length >= MAX_CAPACITY; +export function atCapacity( + flappingSettings: RulesSettingsFlappingProperties, + flappingHistory: boolean[] = [] +): boolean { + return flappingHistory.length >= flappingSettings.lookBackWindow; } diff --git a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts index ecf792518ce8..0c4bf8e04a58 100644 --- a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; import { getAlertsForNotification } from '.'; import { Alert } from '../alert'; @@ -14,6 +15,7 @@ describe('getAlertsForNotification', () => { const alert2 = new Alert('2', { meta: { flapping: false } }); const { newAlerts, activeAlerts } = getAlertsForNotification( + DEFAULT_FLAPPING_SETTINGS, 'default', { '1': alert1, @@ -66,6 +68,7 @@ describe('getAlertsForNotification', () => { const { newAlerts, activeAlerts, recoveredAlerts, currentRecoveredAlerts } = getAlertsForNotification( + DEFAULT_FLAPPING_SETTINGS, 'default', {}, {}, @@ -143,4 +146,117 @@ describe('getAlertsForNotification', () => { } `); }); + + test('should reset counts and not modify alerts if flapping is disabled', () => { + const alert1 = new Alert('1', { + meta: { flapping: true, flappingHistory: [true, false, true], pendingRecoveredCount: 3 }, + }); + const alert2 = new Alert('2', { + meta: { flapping: false, flappingHistory: [true, false, true] }, + }); + const alert3 = new Alert('3', { + meta: { flapping: true, flappingHistory: [true, false, true] }, + }); + + const { newAlerts, activeAlerts, recoveredAlerts, currentRecoveredAlerts } = + getAlertsForNotification( + DISABLE_FLAPPING_SETTINGS, + 'default', + {}, + {}, + { + '1': alert1, + '2': alert2, + '3': alert3, + }, + { + '1': alert1, + '2': alert2, + '3': alert3, + } + ); + + expect(newAlerts).toMatchInlineSnapshot(`Object {}`); + expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); + expect(recoveredAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "2": Object { + "meta": Object { + "flapping": false, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "3": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + } + `); + expect(currentRecoveredAlerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "2": Object { + "meta": Object { + "flapping": false, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + "3": Object { + "meta": Object { + "flapping": true, + "flappingHistory": Array [ + true, + false, + true, + ], + "pendingRecoveredCount": 0, + }, + "state": Object {}, + }, + } + `); + }); }); diff --git a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts index bbfb65ad77b7..e51e35811337 100644 --- a/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts +++ b/x-pack/plugins/alerting/server/lib/get_alerts_for_notification.ts @@ -6,9 +6,9 @@ */ import { keys } from 'lodash'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; -import { MAX_FLAP_COUNT } from './flapping_utils'; export function getAlertsForNotification< State extends AlertInstanceState, @@ -16,6 +16,7 @@ export function getAlertsForNotification< ActionGroupIds extends string, RecoveryActionGroupId extends string >( + flappingSettings: RulesSettingsFlappingProperties, actionGroupId: string, newAlerts: Record> = {}, activeAlerts: Record> = {}, @@ -29,34 +30,38 @@ export function getAlertsForNotification< for (const id of keys(currentRecoveredAlerts)) { const alert = recoveredAlerts[id]; - const flapping = alert.getFlapping(); - if (flapping) { - alert.incrementPendingRecoveredCount(); + if (flappingSettings.enabled) { + const flapping = alert.getFlapping(); + if (flapping) { + alert.incrementPendingRecoveredCount(); - if (alert.getPendingRecoveredCount() < MAX_FLAP_COUNT) { - // keep the context and previous actionGroupId if available - const context = alert.getContext(); - const lastActionGroupId = alert.getLastScheduledActions()?.group; + if (alert.getPendingRecoveredCount() < flappingSettings.statusChangeThreshold) { + // keep the context and previous actionGroupId if available + const context = alert.getContext(); + const lastActionGroupId = alert.getLastScheduledActions()?.group; - const newAlert = new Alert(id, alert.toRaw()); - // unset the end time in the alert state - const state = newAlert.getState(); - delete state.end; - newAlert.replaceState(state); + const newAlert = new Alert(id, alert.toRaw()); + // unset the end time in the alert state + const state = newAlert.getState(); + delete state.end; + newAlert.replaceState(state); - // schedule actions for the new active alert - newAlert.scheduleActions( - (lastActionGroupId ? lastActionGroupId : actionGroupId) as ActionGroupIds, - context - ); - activeAlerts[id] = newAlert; + // schedule actions for the new active alert + newAlert.scheduleActions( + (lastActionGroupId ? lastActionGroupId : actionGroupId) as ActionGroupIds, + context + ); + activeAlerts[id] = newAlert; - // remove from recovered alerts - delete recoveredAlerts[id]; - delete currentRecoveredAlerts[id]; - } else { - alert.resetPendingRecoveredCount(); + // remove from recovered alerts + delete recoveredAlerts[id]; + delete currentRecoveredAlerts[id]; + } else { + alert.resetPendingRecoveredCount(); + } } + } else { + alert.resetPendingRecoveredCount(); } } diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts index 9dae6c4c033a..b1136b6acde4 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.test.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.test.ts @@ -10,6 +10,7 @@ import { cloneDeep } from 'lodash'; import { processAlerts, updateAlertFlappingHistory } from './process_alerts'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; describe('processAlerts', () => { let clock: sinon.SinonFakeTimers; @@ -56,7 +57,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(newAlerts).toEqual({ '1': newAlert }); @@ -94,7 +95,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(newAlerts).toEqual({ '1': newAlert1, '2': newAlert2 }); @@ -140,7 +141,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -178,7 +179,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -226,7 +227,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -284,7 +285,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -345,7 +346,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect( @@ -388,7 +389,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'] }); @@ -416,7 +417,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({}); @@ -446,7 +447,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -485,7 +486,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({ '2': updatedAlerts['2'], '3': updatedAlerts['3'] }); @@ -524,7 +525,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual(updatedAlerts); @@ -554,7 +555,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: false, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({}); @@ -600,7 +601,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 7, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(recoveredAlerts).toEqual({}); @@ -636,7 +637,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 7, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toEqual({ @@ -696,7 +697,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: MAX_ALERTS, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(Object.keys(activeAlerts).length).toEqual(MAX_ALERTS); @@ -730,7 +731,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -781,7 +782,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -818,7 +819,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -874,7 +875,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); @@ -908,7 +909,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(`Object {}`); @@ -950,7 +951,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: false, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1017,7 +1018,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1054,7 +1055,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1116,7 +1117,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: true, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1193,7 +1194,7 @@ describe('processAlerts', () => { hasReachedAlertLimit: true, alertLimit: 10, autoRecoverAlerts: true, - setFlapping: false, + flappingSettings: DISABLE_FLAPPING_SETTINGS, }); expect(activeAlerts).toMatchInlineSnapshot(` @@ -1240,7 +1241,7 @@ describe('processAlerts', () => { const alert = new Alert('1', { meta: { flappingHistory: [false, false] }, }); - updateAlertFlappingHistory(alert, true); + updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true); expect(alert.getFlappingHistory()).toEqual([false, false, true]); }); @@ -1249,7 +1250,7 @@ describe('processAlerts', () => { const alert = new Alert('1', { meta: { flappingHistory }, }); - updateAlertFlappingHistory(alert, true); + updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true); const fh = alert.getFlappingHistory() || []; expect(fh.length).toEqual(20); const result = new Array(19).fill(false); @@ -1261,7 +1262,7 @@ describe('processAlerts', () => { const alert = new Alert('1', { meta: { flappingHistory }, }); - updateAlertFlappingHistory(alert, true); + updateAlertFlappingHistory(DEFAULT_FLAPPING_SETTINGS, alert, true); const fh = alert.getFlappingHistory() || []; expect(fh.length).toEqual(20); const result = new Array(19).fill(false); diff --git a/x-pack/plugins/alerting/server/lib/process_alerts.ts b/x-pack/plugins/alerting/server/lib/process_alerts.ts index 6ce363742a3d..f834f4e4d7b2 100644 --- a/x-pack/plugins/alerting/server/lib/process_alerts.ts +++ b/x-pack/plugins/alerting/server/lib/process_alerts.ts @@ -10,6 +10,7 @@ import { cloneDeep } from 'lodash'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; import { updateFlappingHistory } from './flapping_utils'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; interface ProcessAlertsOpts< State extends AlertInstanceState, @@ -21,8 +22,7 @@ interface ProcessAlertsOpts< hasReachedAlertLimit: boolean; alertLimit: number; autoRecoverAlerts: boolean; - // flag used to determine whether or not we want to push the flapping state on to the flappingHistory array - setFlapping: boolean; + flappingSettings: RulesSettingsFlappingProperties; } interface ProcessAlertsResult< State extends AlertInstanceState, @@ -49,7 +49,7 @@ export function processAlerts< hasReachedAlertLimit, alertLimit, autoRecoverAlerts, - setFlapping, + flappingSettings, }: ProcessAlertsOpts): ProcessAlertsResult< State, Context, @@ -62,14 +62,14 @@ export function processAlerts< existingAlerts, previouslyRecoveredAlerts, alertLimit, - setFlapping + flappingSettings ) : processAlertsHelper( alerts, existingAlerts, previouslyRecoveredAlerts, autoRecoverAlerts, - setFlapping + flappingSettings ); } @@ -83,7 +83,7 @@ function processAlertsHelper< existingAlerts: Record>, previouslyRecoveredAlerts: Record>, autoRecoverAlerts: boolean, - setFlapping: boolean + flappingSettings: RulesSettingsFlappingProperties ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts)); @@ -106,13 +106,13 @@ function processAlertsHelper< const state = newAlerts[id].getState(); newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' }); - if (setFlapping) { + if (flappingSettings.enabled) { if (previouslyRecoveredAlertsIds.has(id)) { // this alert has flapped from recovered to active newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory()); previouslyRecoveredAlertsIds.delete(id); } - updateAlertFlappingHistory(newAlerts[id], true); + updateAlertFlappingHistory(flappingSettings, newAlerts[id], true); } } else { // this alert did exist in previous run @@ -128,8 +128,8 @@ function processAlertsHelper< }); // this alert is still active - if (setFlapping) { - updateAlertFlappingHistory(activeAlerts[id], false); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false); } } } else if (existingAlertIds.has(id) && autoRecoverAlerts) { @@ -147,8 +147,8 @@ function processAlertsHelper< ...(state.start ? { end: currentTime } : {}), }); // this alert has flapped from active to recovered - if (setFlapping) { - updateAlertFlappingHistory(recoveredAlerts[id], true); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], true); } } } @@ -157,8 +157,8 @@ function processAlertsHelper< // alerts are still recovered for (const id of previouslyRecoveredAlertsIds) { recoveredAlerts[id] = previouslyRecoveredAlerts[id]; - if (setFlapping) { - updateAlertFlappingHistory(recoveredAlerts[id], false); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, recoveredAlerts[id], false); } } @@ -175,7 +175,7 @@ function processAlertsLimitReached< existingAlerts: Record>, previouslyRecoveredAlerts: Record>, alertLimit: number, - setFlapping: boolean + flappingSettings: RulesSettingsFlappingProperties ): ProcessAlertsResult { const existingAlertIds = new Set(Object.keys(existingAlerts)); const previouslyRecoveredAlertsIds = new Set(Object.keys(previouslyRecoveredAlerts)); @@ -210,8 +210,8 @@ function processAlertsLimitReached< }); // this alert is still active - if (setFlapping) { - updateAlertFlappingHistory(activeAlerts[id], false); + if (flappingSettings.enabled) { + updateAlertFlappingHistory(flappingSettings, activeAlerts[id], false); } } } @@ -236,12 +236,12 @@ function processAlertsLimitReached< const state = newAlerts[id].getState(); newAlerts[id].replaceState({ ...state, start: currentTime, duration: '0' }); - if (setFlapping) { + if (flappingSettings.enabled) { if (previouslyRecoveredAlertsIds.has(id)) { // this alert has flapped from recovered to active newAlerts[id].setFlappingHistory(previouslyRecoveredAlerts[id].getFlappingHistory()); } - updateAlertFlappingHistory(newAlerts[id], true); + updateAlertFlappingHistory(flappingSettings, newAlerts[id], true); } if (!hasCapacityForNewAlerts()) { @@ -258,7 +258,15 @@ export function updateAlertFlappingHistory< Context extends AlertInstanceContext, ActionGroupIds extends string, RecoveryActionGroupId extends string ->(alert: Alert, state: boolean) { - const updatedFlappingHistory = updateFlappingHistory(alert.getFlappingHistory() || [], state); +>( + flappingSettings: RulesSettingsFlappingProperties, + alert: Alert, + state: boolean +) { + const updatedFlappingHistory = updateFlappingHistory( + flappingSettings, + alert.getFlappingHistory() || [], + state + ); alert.setFlappingHistory(updatedFlappingHistory); } diff --git a/x-pack/plugins/alerting/server/lib/set_flapping.test.ts b/x-pack/plugins/alerting/server/lib/set_flapping.test.ts index 9900d3391861..87d26bc15198 100644 --- a/x-pack/plugins/alerting/server/lib/set_flapping.test.ts +++ b/x-pack/plugins/alerting/server/lib/set_flapping.test.ts @@ -9,6 +9,7 @@ import { pick } from 'lodash'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../../common'; import { setFlapping, isAlertFlapping } from './set_flapping'; +import { DEFAULT_FLAPPING_SETTINGS, DISABLE_FLAPPING_SETTINGS } from '../../common/rules_settings'; describe('setFlapping', () => { const flapping = new Array(16).fill(false).concat([true, true, true, true]); @@ -29,7 +30,7 @@ describe('setFlapping', () => { '4': new Alert('4', { meta: { flapping: true, flappingHistory: notFlapping } }), }; - setFlapping(activeAlerts, recoveredAlerts); + setFlapping(DEFAULT_FLAPPING_SETTINGS, activeAlerts, recoveredAlerts); const fields = ['1.meta.flapping', '2.meta.flapping', '3.meta.flapping', '4.meta.flapping']; expect(pick(activeAlerts, fields)).toMatchInlineSnapshot(` Object { @@ -81,6 +82,73 @@ describe('setFlapping', () => { `); }); + test('should set flapping to false on alerts when flapping is disabled', () => { + const activeAlerts = { + '1': new Alert('1', { meta: { flappingHistory: flapping } }), + '2': new Alert('2', { meta: { flappingHistory: [false, false] } }), + '3': new Alert('3', { meta: { flapping: true, flappingHistory: flapping } }), + '4': new Alert('4', { meta: { flapping: true, flappingHistory: [false, false] } }), + }; + + const recoveredAlerts = { + '1': new Alert('1', { meta: { flappingHistory: [true, true, true, true] } }), + '2': new Alert('2', { meta: { flappingHistory: notFlapping } }), + '3': new Alert('3', { meta: { flapping: true, flappingHistory: [true, true] } }), + '4': new Alert('4', { meta: { flapping: true, flappingHistory: notFlapping } }), + }; + + setFlapping(DISABLE_FLAPPING_SETTINGS, activeAlerts, recoveredAlerts); + const fields = ['1.meta.flapping', '2.meta.flapping', '3.meta.flapping', '4.meta.flapping']; + expect(pick(activeAlerts, fields)).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": false, + }, + }, + "2": Object { + "meta": Object { + "flapping": false, + }, + }, + "3": Object { + "meta": Object { + "flapping": false, + }, + }, + "4": Object { + "meta": Object { + "flapping": false, + }, + }, + } + `); + expect(pick(recoveredAlerts, fields)).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "flapping": false, + }, + }, + "2": Object { + "meta": Object { + "flapping": false, + }, + }, + "3": Object { + "meta": Object { + "flapping": false, + }, + }, + "4": Object { + "meta": Object { + "flapping": false, + }, + }, + } + `); + }); + describe('isAlertFlapping', () => { describe('not currently flapping', () => { test('returns true if the flap count exceeds the threshold', () => { @@ -91,7 +159,7 @@ describe('setFlapping', () => { meta: { flappingHistory }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test("returns false the flap count doesn't exceed the threshold", () => { @@ -102,7 +170,7 @@ describe('setFlapping', () => { meta: { flappingHistory }, } ); - expect(isAlertFlapping(alert)).toEqual(false); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(false); }); test('returns true if not at capacity and the flap count exceeds the threshold', () => { @@ -113,7 +181,7 @@ describe('setFlapping', () => { meta: { flappingHistory }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); }); @@ -126,7 +194,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test("returns true if not at capacity and the flap count doesn't exceed the threshold", () => { @@ -137,7 +205,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test('returns true if not at capacity and the flap count exceeds the threshold', () => { @@ -148,7 +216,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(true); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(true); }); test("returns false if at capacity and the flap count doesn't exceed the threshold", () => { @@ -159,7 +227,7 @@ describe('setFlapping', () => { meta: { flappingHistory, flapping: true }, } ); - expect(isAlertFlapping(alert)).toEqual(false); + expect(isAlertFlapping(DEFAULT_FLAPPING_SETTINGS, alert)).toEqual(false); }); }); }); diff --git a/x-pack/plugins/alerting/server/lib/set_flapping.ts b/x-pack/plugins/alerting/server/lib/set_flapping.ts index 2e941cf06e07..ee5871fba965 100644 --- a/x-pack/plugins/alerting/server/lib/set_flapping.ts +++ b/x-pack/plugins/alerting/server/lib/set_flapping.ts @@ -9,6 +9,7 @@ import { keys } from 'lodash'; import { Alert } from '../alert'; import { AlertInstanceState, AlertInstanceContext } from '../types'; import { isFlapping } from './flapping_utils'; +import { RulesSettingsFlappingProperties } from '../../common/rules_settings'; export function setFlapping< State extends AlertInstanceState, @@ -16,18 +17,19 @@ export function setFlapping< ActionGroupIds extends string, RecoveryActionGroupIds extends string >( + flappingSettings: RulesSettingsFlappingProperties, activeAlerts: Record> = {}, recoveredAlerts: Record> = {} ) { for (const id of keys(activeAlerts)) { const alert = activeAlerts[id]; - const flapping = isAlertFlapping(alert); + const flapping = isAlertFlapping(flappingSettings, alert); alert.setFlapping(flapping); } for (const id of keys(recoveredAlerts)) { const alert = recoveredAlerts[id]; - const flapping = isAlertFlapping(alert); + const flapping = isAlertFlapping(flappingSettings, alert); alert.setFlapping(flapping); } } @@ -37,8 +39,13 @@ export function isAlertFlapping< Context extends AlertInstanceContext, ActionGroupIds extends string, RecoveryActionGroupId extends string ->(alert: Alert): boolean { +>( + flappingSettings: RulesSettingsFlappingProperties, + alert: Alert +): boolean { const flappingHistory: boolean[] = alert.getFlappingHistory() || []; const isCurrentlyFlapping = alert.getFlapping(); - return isFlapping(flappingHistory, isCurrentlyFlapping); + return flappingSettings.enabled + ? isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping) + : false; } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 13f6e2ae1c9d..6070b5cee56f 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -464,6 +464,10 @@ export class AlertingPlugin { return alertingAuthorizationClientFactory!.create(request); }; + const getRulesSettingsClientWithRequest = (request: KibanaRequest) => { + return rulesSettingsClientFactory!.create(request); + }; + taskRunnerFactory.initialize({ logger, data: plugins.data, @@ -488,6 +492,7 @@ export class AlertingPlugin { maxAlerts: this.config.rules.run.alerts.max, actionsConfigMap: getActionsConfigMap(this.config.rules.run.actions), usageCounter: this.usageCounter, + getRulesSettingsClientWithRequest, }); this.eventLogService!.registerSavedObjectProvider('alert', (request) => { diff --git a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts index 2c321e54ebf7..99dcfc388ca2 100644 --- a/x-pack/plugins/alerting/server/rules_settings_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_settings_client.mock.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { RulesSettingsClientApi, RulesSettingsFlappingClientApi } from './types'; +import { + RulesSettingsClientApi, + RulesSettingsFlappingClientApi, + DEFAULT_FLAPPING_SETTINGS, +} from './types'; export type RulesSettingsClientMock = jest.Mocked; export type RulesSettingsFlappingClientMock = jest.Mocked; @@ -14,7 +18,7 @@ export type RulesSettingsFlappingClientMock = jest.Mocked { const flappingMocked: RulesSettingsFlappingClientMock = { - get: jest.fn(), + get: jest.fn().mockReturnValue(DEFAULT_FLAPPING_SETTINGS), update: jest.fn(), }; const mocked: RulesSettingsClientMock = { 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 e27bb167d45e..d840a9397e24 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 @@ -76,6 +76,7 @@ import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_e import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import { rulesSettingsClientMock } from '../rules_settings_client.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -162,6 +163,7 @@ describe('Task Runner', () => { max: 10000, }, }, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), }; const ephemeralTestParams: Array< @@ -209,6 +211,9 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); + taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( + rulesSettingsClientMock.create() + ); mockedRuleTypeSavedObject.monitoring!.run.history = []; mockedRuleTypeSavedObject.monitoring!.run.calculated_metrics.success_ratio = 0; 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 aa07922aa741..808fe89baa7c 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -295,6 +295,8 @@ export class TaskRunner< ...wrappedClientOptions, searchSourceClient, }); + const rulesSettingsClient = this.context.getRulesSettingsClientWithRequest(fakeRequest); + const flappingSettings = await rulesSettingsClient.flapping().get(); const { updatedRuleTypeState } = await this.timer.runWithTimer( TaskRunnerTimerSpan.RuleTypeRun, @@ -373,6 +375,7 @@ export class TaskRunner< notifyWhen, }, logger: this.logger, + flappingSettings, }) ); @@ -418,6 +421,7 @@ export class TaskRunner< ruleLabel, ruleRunMetricsStore, shouldLogAndScheduleActionsForAlerts: this.shouldLogAndScheduleActionsForAlerts(), + flappingSettings, }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 6090df1230b2..6c094d330bf1 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -53,6 +53,7 @@ import { EVENT_LOG_ACTIONS } from '../plugin'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { rulesSettingsClientMock } from '../rules_settings_client.mock'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -138,6 +139,7 @@ describe('Task Runner Cancel', () => { max: 1000, }, }, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), }; beforeEach(() => { @@ -165,6 +167,9 @@ describe('Task Runner Cancel', () => { taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => fn() ); + taskRunnerFactoryInitializerParams.getRulesSettingsClientWithRequest.mockReturnValue( + rulesSettingsClientMock.create() + ); rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 9864a6caafc8..534ab1dc972d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -29,6 +29,7 @@ import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { rulesSettingsClientMock } from '../rules_settings_client.mock'; const inMemoryMetrics = inMemoryMetricsMock.create(); const executionContext = executionContextServiceMock.createSetupContract(); @@ -115,6 +116,7 @@ describe('Task Runner Factory', () => { max: 1000, }, }, + getRulesSettingsClientWithRequest: jest.fn().mockReturnValue(rulesSettingsClientMock.create()), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index c323a87d4522..5a0224a6f11e 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -31,6 +31,7 @@ import { AlertInstanceState, AlertInstanceContext, RulesClientApi, + RulesSettingsClientApi, } from '../types'; import { TaskRunner } from './task_runner'; import { NormalizedRuleType } from '../rule_type_registry'; @@ -61,6 +62,7 @@ export interface TaskRunnerContext { actionsConfigMap: ActionsConfigMap; cancelAlertsOnRuleTimeout: boolean; usageCounter?: UsageCounter; + getRulesSettingsClientWithRequest(request: KibanaRequest): RulesSettingsClientApi; } export class TaskRunnerFactory { diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 4092b9209432..09493e4357a1 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -51,6 +51,7 @@ import { } from '../common'; import { PublicAlertFactory } from './alert/create_alert_factory'; import { FieldMap } from '../common/alert_schema/field_maps/types'; +import { RulesSettingsFlappingProperties } from '../common/rules_settings'; export type WithoutQueryAndParams = Pick>; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export type { RuleTypeParams }; @@ -111,6 +112,7 @@ export interface RuleExecutorOptions< startedAt: Date; state: State; namespace?: string; + flappingSettings: RulesSettingsFlappingProperties; } export interface RuleParamsAndRefs { diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index 41d7385a2c3d..a61dea317f66 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -12,6 +12,7 @@ import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; import { PluginSetupContract as AlertingPluginSetupContract } from '@kbn/alerting-plugin/server'; import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..'; export const createRuleTypeMocks = () => { @@ -80,6 +81,7 @@ export const createRuleTypeMocks = () => { ruleTypeName: 'ruleTypeName', }, startedAt: new Date(), + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); }, }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 92fbc186dce5..85059a2ee623 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -33,6 +33,7 @@ import { } from './metric_threshold_executor'; import { Evaluation } from './lib/evaluate_rule'; import type { LogMeta, Logger } from '@kbn/logging'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() })); @@ -116,6 +117,7 @@ const mockOptions = { ruleTypeName: '', }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }; const setEvaluationResults = (response: Array>) => { diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts index 2004fbb8428c..97ffcbd1a722 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/executor.test.ts @@ -19,6 +19,7 @@ import { ISearchStartSearchSource } from '@kbn/data-plugin/public'; import { MockedLogger } from '@kbn/logging-mocks'; import { SanitizedRuleConfig } from '@kbn/alerting-plugin/common'; import { Alert, RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, @@ -118,6 +119,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); @@ -142,6 +144,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); @@ -166,6 +169,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); @@ -195,6 +199,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).toBeCalledWith({ @@ -242,6 +247,7 @@ describe('BurnRateRuleExecutor', () => { rule: {} as SanitizedRuleConfig, spaceId: 'irrelevant', state: {}, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertWithLifecycleMock).not.toBeCalled(); diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index e3678e145552..7f22b218ec0f 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -162,6 +162,7 @@ export const createLifecycleExecutor = const { services: { alertFactory, shouldWriteAlerts }, state: previousState, + flappingSettings, } = options; const ruleDataClientWriter = await ruleDataClient.getWriter(); @@ -266,6 +267,7 @@ export const createLifecycleExecutor = const isActive = !isRecovered; const flappingHistory = getUpdatedFlappingHistory( + flappingSettings, alertId, state, isNew, @@ -290,7 +292,7 @@ export const createLifecycleExecutor = pendingRecoveredCount: 0, }; - const flapping = isFlapping(flappingHistory, isCurrentlyFlapping); + const flapping = isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping); const event: ParsedTechnicalFields & ParsedExperimentalFields = { ...alertData?.fields, @@ -329,7 +331,7 @@ export const createLifecycleExecutor = const newEventsToIndex = makeEventsDataMapFor(newAlertIds); const trackedRecoveredEventsToIndex = makeEventsDataMapFor(trackedAlertRecoveredIds); const allEventsToIndex = [ - ...getAlertsForNotification(trackedEventsToIndex), + ...getAlertsForNotification(flappingSettings, trackedEventsToIndex), ...newEventsToIndex, ]; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index de164dd07c0b..92cbdb35240b 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -22,6 +22,7 @@ import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_fac import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; type RuleTestHelpers = ReturnType; @@ -138,6 +139,7 @@ function createRule(shouldWriteAlerts: boolean = true) { spaceId: 'spaceId', startedAt, state, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, })) ?? {}) as Record); previousStartedAt = startedAt; diff --git a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts index 08a6c90eda02..b3047303bcb0 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.test.ts @@ -5,7 +5,12 @@ * 2.0. */ +import { + DEFAULT_FLAPPING_SETTINGS, + DISABLE_FLAPPING_SETTINGS, +} from '@kbn/alerting-plugin/common/rules_settings'; import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; +import { cloneDeep } from 'lodash'; import { getAlertsForNotification } from './get_alerts_for_notification'; describe('getAlertsForNotification', () => { @@ -38,7 +43,8 @@ describe('getAlertsForNotification', () => { test('should set pendingRecoveredCount to zero for all active alerts', () => { const trackedEvents = [alert4]; - expect(getAlertsForNotification(trackedEvents)).toMatchInlineSnapshot(` + expect(getAlertsForNotification(DEFAULT_FLAPPING_SETTINGS, trackedEvents)) + .toMatchInlineSnapshot(` Array [ Object { "event": Object { @@ -55,8 +61,9 @@ describe('getAlertsForNotification', () => { }); test('should not remove alerts if the num of recovered alerts is not at the limit', () => { - const trackedEvents = [alert1, alert2, alert3]; - expect(getAlertsForNotification(trackedEvents)).toMatchInlineSnapshot(` + const trackedEvents = cloneDeep([alert1, alert2, alert3]); + expect(getAlertsForNotification(DEFAULT_FLAPPING_SETTINGS, trackedEvents)) + .toMatchInlineSnapshot(` Array [ Object { "event": Object { @@ -82,4 +89,34 @@ describe('getAlertsForNotification', () => { ] `); }); + + test('should reset counts and not modify alerts if flapping is disabled', () => { + const trackedEvents = cloneDeep([alert1, alert2, alert3]); + expect(getAlertsForNotification(DISABLE_FLAPPING_SETTINGS, trackedEvents)) + .toMatchInlineSnapshot(` + Array [ + Object { + "event": Object { + "kibana.alert.status": "recovered", + }, + "flapping": true, + "pendingRecoveredCount": 0, + }, + Object { + "event": Object { + "kibana.alert.status": "recovered", + }, + "flapping": false, + "pendingRecoveredCount": 0, + }, + Object { + "event": Object { + "kibana.alert.status": "recovered", + }, + "flapping": true, + "pendingRecoveredCount": 0, + }, + ] + `); + }); }); diff --git a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts index 75d07642c5e5..878db2a91802 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_alerts_for_notification.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MAX_FLAP_COUNT } from '@kbn/alerting-plugin/server/lib/flapping_utils'; +import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common/rules_settings'; import { ALERT_END, ALERT_STATUS, @@ -14,15 +14,21 @@ import { EVENT_ACTION, } from '@kbn/rule-data-utils'; -export function getAlertsForNotification(trackedEventsToIndex: any[]) { +export function getAlertsForNotification( + flappingSettings: RulesSettingsFlappingProperties, + trackedEventsToIndex: any[] +) { return trackedEventsToIndex.map((trackedEvent) => { - if (trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_ACTIVE) { + if (!flappingSettings.enabled || trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_ACTIVE) { trackedEvent.pendingRecoveredCount = 0; - } else if (trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_RECOVERED) { + } else if ( + flappingSettings.enabled && + trackedEvent.event[ALERT_STATUS] === ALERT_STATUS_RECOVERED + ) { if (trackedEvent.flapping) { const count = trackedEvent.pendingRecoveredCount || 0; trackedEvent.pendingRecoveredCount = count + 1; - if (trackedEvent.pendingRecoveredCount < MAX_FLAP_COUNT) { + if (trackedEvent.pendingRecoveredCount < flappingSettings.statusChangeThreshold) { trackedEvent.event[ALERT_STATUS] = ALERT_STATUS_ACTIVE; trackedEvent.event[EVENT_ACTION] = 'active'; delete trackedEvent.event[ALERT_END]; diff --git a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts index 2194d37360f1..52467f168e64 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts @@ -5,6 +5,10 @@ * 2.0. */ +import { + DEFAULT_FLAPPING_SETTINGS, + DISABLE_FLAPPING_SETTINGS, +} from '@kbn/alerting-plugin/common/rules_settings'; import { getUpdatedFlappingHistory } from './get_updated_flapping_history'; describe('getUpdatedFlappingHistory', () => { @@ -17,8 +21,17 @@ describe('getUpdatedFlappingHistory', () => { test('sets flapping state to true if the alert is new', () => { const state = { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, true, false, false, [])) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + true, + false, + false, + [] + ) + ).toMatchInlineSnapshot(` Array [ true, ] @@ -40,8 +53,17 @@ describe('getUpdatedFlappingHistory', () => { }, trackedAlertsRecovered: {}, }; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, false, true, [])) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + false, + true, + [] + ) + ).toMatchInlineSnapshot(` Array [ false, ] @@ -64,8 +86,17 @@ describe('getUpdatedFlappingHistory', () => { trackedAlerts: {}, }; const recoveredIds = ['TEST_ALERT_0']; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, true, false, true, recoveredIds)) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + true, + false, + true, + recoveredIds + ) + ).toMatchInlineSnapshot(` Array [ true, ] @@ -89,8 +120,17 @@ describe('getUpdatedFlappingHistory', () => { trackedAlertsRecovered: {}, }; const recoveredIds = ['TEST_ALERT_0']; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, true, false, recoveredIds)) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + true, + false, + recoveredIds + ) + ).toMatchInlineSnapshot(` Array [ true, ] @@ -98,7 +138,7 @@ describe('getUpdatedFlappingHistory', () => { expect(recoveredIds).toEqual(['TEST_ALERT_0']); }); - test('sets flapping state to true on an alert that is still recovered', () => { + test('sets flapping state to false on an alert that is still recovered', () => { const state = { wrapped: initialRuleState, trackedAlerts: {}, @@ -114,12 +154,49 @@ describe('getUpdatedFlappingHistory', () => { }, }; const recoveredIds = ['TEST_ALERT_0']; - expect(getUpdatedFlappingHistory('TEST_ALERT_0', state, false, true, false, recoveredIds)) - .toMatchInlineSnapshot(` + expect( + getUpdatedFlappingHistory( + DEFAULT_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + true, + false, + recoveredIds + ) + ).toMatchInlineSnapshot(` Array [ false, ] `); expect(recoveredIds).toEqual(['TEST_ALERT_0']); }); + + test('does not set flapping state if flapping is not enabled', () => { + const state = { + wrapped: initialRuleState, + trackedAlerts: {}, + trackedAlertsRecovered: { + TEST_ALERT_0: { + alertId: 'TEST_ALERT_0', + alertUuid: 'TEST_ALERT_0_UUID', + started: '2020-01-01T12:00:00.000Z', + flappingHistory: [], + flapping: false, + pendingRecoveredCount: 0, + }, + }, + }; + expect( + getUpdatedFlappingHistory( + DISABLE_FLAPPING_SETTINGS, + 'TEST_ALERT_0', + state, + false, + true, + false, + ['TEST_ALERT_0'] + ) + ).toMatchInlineSnapshot(`Array []`); + }); }); diff --git a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts index 0f64e5778f92..854f91972233 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts @@ -6,11 +6,13 @@ */ import { RuleTypeState } from '@kbn/alerting-plugin/common'; +import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common/rules_settings'; import { updateFlappingHistory } from '@kbn/alerting-plugin/server/lib'; import { remove } from 'lodash'; import { WrappedLifecycleRuleState } from './create_lifecycle_executor'; export function getUpdatedFlappingHistory( + flappingSettings: RulesSettingsFlappingProperties, alertId: string, state: WrappedLifecycleRuleState, isNew: boolean, @@ -20,31 +22,43 @@ export function getUpdatedFlappingHistory( ) { // duplicating this logic to determine flapping at this level let flappingHistory: boolean[] = []; - if (isRecovered) { - if (state.trackedAlerts[alertId]) { - // this alert has flapped from active to recovered - flappingHistory = updateFlappingHistory(state.trackedAlerts[alertId].flappingHistory, true); - } else if (state.trackedAlertsRecovered[alertId]) { - // this alert is still recovered + if (flappingSettings.enabled) { + if (isRecovered) { + if (state.trackedAlerts[alertId]) { + // this alert has flapped from active to recovered + flappingHistory = updateFlappingHistory( + flappingSettings, + state.trackedAlerts[alertId].flappingHistory, + true + ); + } else if (state.trackedAlertsRecovered[alertId]) { + // this alert is still recovered + flappingHistory = updateFlappingHistory( + flappingSettings, + state.trackedAlertsRecovered[alertId].flappingHistory, + false + ); + } + } else if (isNew) { + if (state.trackedAlertsRecovered[alertId]) { + // this alert has flapped from recovered to active + flappingHistory = updateFlappingHistory( + flappingSettings, + state.trackedAlertsRecovered[alertId].flappingHistory, + true + ); + remove(recoveredIds, (id) => id === alertId); + } else { + flappingHistory = updateFlappingHistory(flappingSettings, [], true); + } + } else if (isActive) { + // this alert is still active flappingHistory = updateFlappingHistory( - state.trackedAlertsRecovered[alertId].flappingHistory, + flappingSettings, + state.trackedAlerts[alertId].flappingHistory, false ); } - } else if (isNew) { - if (state.trackedAlertsRecovered[alertId]) { - // this alert has flapped from recovered to active - flappingHistory = updateFlappingHistory( - state.trackedAlertsRecovered[alertId].flappingHistory, - true - ); - remove(recoveredIds, (id) => id === alertId); - } else { - flappingHistory = updateFlappingHistory([], true); - } - } else if (isActive) { - // this alert is still active - flappingHistory = updateFlappingHistory(state.trackedAlerts[alertId].flappingHistory, false); } return flappingHistory; } diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts index 9e75fa159190..f2416b0cba67 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor.test_helpers.ts @@ -21,6 +21,7 @@ import { searchSourceCommonMock } from '@kbn/data-plugin/common/search/search_so import { Logger } from '@kbn/logging'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; export const createDefaultAlertExecutorOptions = < Params extends RuleTypeParams = never, @@ -87,4 +88,5 @@ export const createDefaultAlertExecutorOptions = < namespace: undefined, executionId: 'b33f65d7-6e8b-4aae-8d20-c93613deb33f', logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts index 2ffbde91145c..de04cac5f1fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.test.ts @@ -8,6 +8,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { getRuleMock } from '../../../routes/__mocks__/request_responses'; // eslint-disable-next-line no-restricted-imports @@ -67,6 +68,7 @@ describe('legacyRules_notification_alert_type', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }; alert = legacyRulesNotificationAlertType({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 457412dd0150..6491aeb96f5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -15,7 +15,7 @@ import type { AlertInstanceState, RuleTypeState, } from '@kbn/alerting-plugin/common'; -import { parseDuration } from '@kbn/alerting-plugin/common'; +import { parseDuration, DISABLE_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import type { ExecutorType } from '@kbn/alerting-plugin/server/types'; import type { Alert } from '@kbn/alerting-plugin/server'; @@ -263,6 +263,7 @@ export const previewRulesRoute = async ( startedAt: startedAt.toDate(), state: statePreview, logger, + flappingSettings: DISABLE_FLAPPING_SETTINGS, })) as { state: TState }); const errors = loggedStatusChanges diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts index 8669e66d1026..825870a0a30c 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts @@ -24,6 +24,7 @@ import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; import { OnlyEsQueryRuleParams, OnlySearchSourceRuleParams } from './types'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { Comparator } from '../../../common/comparator_types'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; const logger = loggingSystemMock.create().get(); const coreSetup = coreMock.createSetup(); @@ -726,5 +727,6 @@ async function invokeExecutor({ notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts index 5b3ed6ebec8b..c4e9078c90c2 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts @@ -16,6 +16,7 @@ import { Params } from './rule_type_params'; import { TIME_SERIES_BUCKET_SELECTOR_FIELD } from '@kbn/triggers-actions-ui-plugin/server'; import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { Comparator } from '../../../common/comparator_types'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; let fakeTimer: sinon.SinonFakeTimers; @@ -217,6 +218,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(alertServices.alertFactory.create).toHaveBeenCalledWith('all documents'); @@ -280,6 +282,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); @@ -343,6 +346,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(customAlertServices.alertFactory.create).not.toHaveBeenCalled(); @@ -405,6 +409,7 @@ describe('ruleType', () => { notifyWhen: null, }, logger, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, }); expect(data.timeSeriesQuery).toHaveBeenCalledWith( diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index 2d393f1f7cdb..2c0651f3f962 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -19,3 +19,4 @@ export * from './test_assertions'; export { checkAAD } from './check_aad'; export { getEventLog } from './get_event_log'; export { createWaitForExecutionCount } from './wait_for_execution_count'; +export { resetRulesSettings } from './reset_rules_settings'; diff --git a/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts new file mode 100644 index 000000000000..b1b24856e9ef --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/reset_rules_settings.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; +import { Superuser } from '../../security_and_spaces/scenarios'; +import { getUrlPrefix } from './space_test_utils'; + +export const resetRulesSettings = (supertest: any, space: string) => { + return supertest + .post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`) + .set('kbn-xsrf', 'foo') + .auth(Superuser.username, Superuser.password) + .send(DEFAULT_FLAPPING_SETTINGS) + .expect(200); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts index 80e0a3e4a598..7bc307f41e6d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/get_flapping_settings.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { UserAtSpaceScenarios } from '../../../scenarios'; -import { getUrlPrefix } from '../../../../common/lib'; +import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -16,6 +16,16 @@ export default function getFlappingSettingsTests({ getService }: FtrProviderCont const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('getFlappingSettings', () => { + beforeEach(async () => { + await resetRulesSettings(supertestWithoutAuth, 'space1'); + await resetRulesSettings(supertestWithoutAuth, 'space2'); + }); + + after(async () => { + await resetRulesSettings(supertestWithoutAuth, 'space1'); + await resetRulesSettings(supertestWithoutAuth, 'space2'); + }); + for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; describe(scenario.id, () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts index 29c82ee5e642..93659256d2e9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/update_flapping_settings.ts @@ -6,19 +6,10 @@ */ import expect from '@kbn/expect'; -import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common'; import { UserAtSpaceScenarios, Superuser } from '../../../scenarios'; -import { getUrlPrefix } from '../../../../common/lib'; +import { getUrlPrefix, resetRulesSettings } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -const resetRulesSettings = (supertestWithoutAuth: any, space: string) => { - return supertestWithoutAuth - .post(`${getUrlPrefix(space)}/internal/alerting/rules/settings/_flapping`) - .set('kbn-xsrf', 'foo') - .auth(Superuser.username, Superuser.password) - .send(DEFAULT_FLAPPING_SETTINGS); -}; - // eslint-disable-next-line import/no-default-export export default function updateFlappingSettingsTest({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index 9a7d90d4adf2..5525631c2a53 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -9,7 +9,13 @@ import expect from '@kbn/expect'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; import { Spaces } from '../../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData, + ObjectRemover, + getEventLog, + resetRulesSettings, +} from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -23,10 +29,17 @@ export default function eventLogTests({ getService }: FtrProviderContext) { const objectRemover = new ObjectRemover(supertest); beforeEach(async () => { + await resetRulesSettings(supertest, Spaces.default.id); + await resetRulesSettings(supertest, Spaces.space1.id); await esTestIndexTool.destroy(); await esTestIndexTool.setup(); }); + after(async () => { + await resetRulesSettings(supertest, Spaces.default.id); + await resetRulesSettings(supertest, Spaces.space1.id); + }); + afterEach(async () => { await objectRemover.removeAll(); }); @@ -527,6 +540,16 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); it('should generate expected events for flapping alerts that are mainly active', async () => { + await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`) + .set('kbn-xsrf', 'foo') + .auth('superuser', 'superuser') + .send({ + enabled: true, + lookBackWindow: 3, + statusChangeThreshold: 2, + }) + .expect(200); const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -539,7 +562,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .expect(200); // pattern of when the alert should fire - const instance = [true, false, true, false].concat(new Array(22).fill(true)); + const instance = [true, false, true, true, true, true, true]; const pattern = { instance, }; @@ -579,12 +602,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { provider: 'alerting', actions: new Map([ // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 25 }], - ['execute', { gte: 25 }], - ['execute-action', { equal: 25 }], - ['new-instance', { equal: 2 }], - ['active-instance', { gte: 25 }], - ['recovered-instance', { equal: 2 }], + ['execute-start', { gte: 6 }], + ['execute', { gte: 6 }], + ['execute-action', { equal: 7 }], + ['new-instance', { equal: 1 }], + ['active-instance', { gte: 6 }], + ['recovered-instance', { equal: 1 }], ]), }); }); @@ -596,13 +619,21 @@ export default function eventLogTests({ getService }: FtrProviderContext) { event?.event?.action === 'recovered-instance' ) .map((event) => event?.kibana?.alert?.flapping); - const result = [false, false, false] - .concat(new Array(20).fill(true)) - .concat([false, false, false, false]); + const result = [false, true, true, true, false, false, false, false]; expect(flapping).to.eql(result); }); it('should generate expected events for flapping alerts that are mainly recovered', async () => { + await supertest + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`) + .set('kbn-xsrf', 'foo') + .auth('superuser', 'superuser') + .send({ + enabled: true, + lookBackWindow: 3, + statusChangeThreshold: 2, + }) + .expect(200); const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') @@ -615,7 +646,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { .expect(200); // pattern of when the alert should fire - const instance = [true, false, true].concat(new Array(18).fill(false)).concat(true); + const instance = [true, false, true, false, false, false, true]; const pattern = { instance, }; @@ -655,12 +686,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { provider: 'alerting', actions: new Map([ // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 20 }], - ['execute', { gte: 20 }], - ['execute-action', { equal: 9 }], - ['new-instance', { equal: 3 }], - ['active-instance', { gte: 9 }], - ['recovered-instance', { equal: 3 }], + ['execute-start', { gte: 6 }], + ['execute', { gte: 6 }], + ['execute-action', { equal: 6 }], + ['new-instance', { equal: 2 }], + ['active-instance', { gte: 6 }], + ['recovered-instance', { equal: 2 }], ]), }); }); @@ -672,20 +703,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { event?.event?.action === 'recovered-instance' ) .map((event) => event?.kibana?.alert?.flapping); - expect(flapping).to.eql([ - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true, - true, - ]); + expect(flapping).to.eql([false, true, true, true, true, true, true, true]); }); }); } diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts index c888269dfdb2..b62d744503fc 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_summarized_alerts.ts @@ -28,6 +28,7 @@ import { RuleDataService, } from '@kbn/rule-registry-plugin/server'; import { RuleExecutorOptions } from '@kbn/alerting-plugin/server'; +import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; import { get } from 'lodash'; import type { FtrProviderContext } from '../../../common/ftr_provider_context'; import { @@ -171,6 +172,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide alertFactory: { create: sinon.stub() }, shouldWriteAlerts: sinon.stub().returns(true), }, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, } as unknown as RuleExecutorOptions< MockRuleParams, WrappedLifecycleRuleState<{ shouldTriggerAlert: boolean }>, @@ -329,6 +331,7 @@ export default function createGetSummarizedAlertsTest({ getService }: FtrProvide alertFactory: { create: sinon.stub() }, shouldWriteAlerts: sinon.stub().returns(true), }, + flappingSettings: DEFAULT_FLAPPING_SETTINGS, } as unknown as RuleExecutorOptions< MockRuleParams, WrappedLifecycleRuleState<{ shouldTriggerAlert: boolean }>, From b130d5dc73f1c2c414d187850347607cbb972b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 2 Feb 2023 15:05:26 +0100 Subject: [PATCH 07/35] [APM] Add `service.environment` log correlation (#150065) Pitch: https://github.com/elastic/apm-dev/issues/938 Follow-up to https://github.com/elastic/kibana/pull/150005 cc @SylvainJuge --- .../components/app/service_logs/index.test.ts | 40 ++++++++++------- .../components/app/service_logs/index.tsx | 44 ++++++++++++++----- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts index 2c3f5b49460f..f62e86117374 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts @@ -8,47 +8,55 @@ import { getInfrastructureKQLFilter } from '.'; describe('service logs', () => { const serviceName = 'opbeans-node'; + const environment = 'production'; describe('getInfrastructureKQLFilter', () => { - it('filter by service name', () => { + it('filter by service name and environment', () => { expect( - getInfrastructureKQLFilter( - { + getInfrastructureKQLFilter({ + data: { containerIds: [], hostNames: [], podNames: [], }, - serviceName - ) - ).toEqual('service.name: "opbeans-node"'); + serviceName, + environment, + }) + ).toEqual( + '(service.name: "opbeans-node" and service.environment: "production") or (service.name: "opbeans-node" and not service.environment: *)' + ); }); it('filter by container id as fallback', () => { expect( - getInfrastructureKQLFilter( - { + getInfrastructureKQLFilter({ + data: { containerIds: ['foo', 'bar'], hostNames: ['baz', `quz`], podNames: [], }, - serviceName - ) + serviceName, + environment, + }) ).toEqual( - 'service.name: "opbeans-node" or (not service.name and (container.id: "foo" or container.id: "bar"))' + '(service.name: "opbeans-node" and service.environment: "production") or (service.name: "opbeans-node" and not service.environment: *) or ((container.id: "foo" or container.id: "bar") and not service.name: *)' ); }); it('does not filter by host names as fallback', () => { expect( - getInfrastructureKQLFilter( - { + getInfrastructureKQLFilter({ + data: { containerIds: [], hostNames: ['baz', `quz`], podNames: [], }, - serviceName - ) - ).toEqual('service.name: "opbeans-node"'); + serviceName, + environment, + }) + ).toEqual( + '(service.name: "opbeans-node" and service.environment: "production") or (service.name: "opbeans-node" and not service.environment: *)' + ); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index 02cdf9679398..11e12e0c177c 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -12,7 +12,11 @@ import { useFetcher } from '../../../hooks/use_fetcher'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; -import { CONTAINER_ID, SERVICE_NAME } from '../../../../common/es_fields/apm'; +import { + CONTAINER_ID, + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '../../../../common/es_fields/apm'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -54,24 +58,40 @@ export function ServiceLogs() { height={'60vh'} startTimestamp={moment(start).valueOf()} endTimestamp={moment(end).valueOf()} - query={getInfrastructureKQLFilter(data, serviceName)} + query={getInfrastructureKQLFilter({ data, serviceName, environment })} showFlyoutAction /> ); } -export const getInfrastructureKQLFilter = ( +export function getInfrastructureKQLFilter({ + data, + serviceName, + environment, +}: { data: | APIReturnType<'GET /internal/apm/services/{serviceName}/infrastructure_attributes'> - | undefined, - serviceName: string -) => { - const containerIds: string[] = data?.containerIds ?? []; - const containerIdKql = containerIds + | undefined; + serviceName: string; + environment: string; +}) { + // correlate on service.name + service.environment + const serviceNameAndEnvironmentCorrelation = `(${SERVICE_NAME}: "${serviceName}" and ${SERVICE_ENVIRONMENT}: "${environment}")`; + + // correlate on service.name + const serviceNameCorrelation = `(${SERVICE_NAME}: "${serviceName}" and not ${SERVICE_ENVIRONMENT}: *)`; + + // correlate on container.id + const containerIdKql = (data?.containerIds ?? []) .map((id) => `${CONTAINER_ID}: "${id}"`) .join(' or '); + const containerIdCorrelation = containerIdKql + ? [`((${containerIdKql}) and not ${SERVICE_NAME}: *)`] + : []; - return containerIds.length - ? `${SERVICE_NAME}: "${serviceName}" or (not ${SERVICE_NAME} and (${containerIdKql}))` - : `${SERVICE_NAME}: "${serviceName}"`; -}; + return [ + serviceNameAndEnvironmentCorrelation, + serviceNameCorrelation, + ...containerIdCorrelation, + ].join(' or '); +} From 921e4b128135251522732928151fd884fbfcc1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 2 Feb 2023 09:06:39 -0500 Subject: [PATCH 08/35] [APM] Cannot read/write APM Settings Indices page with minimally-privileged user (#150107) closes https://github.com/elastic/kibana/issues/126483 Screenshot 2023-02-01 at 3 44 04 PM Screenshot 2023-02-01 at 3 44 39 PM --- .../components/app/settings/apm_indices/index.tsx | 11 +++-------- x-pack/plugins/apm/server/feature.ts | 5 +++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx index 419229fe3ac0..f551a9b6c51f 100644 --- a/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/apm_indices/index.tsx @@ -98,14 +98,9 @@ export function ApmIndices() { const [apmIndices, setApmIndices] = useState>({}); const [isSaving, setIsSaving] = useState(false); - const { data = INITIAL_STATE, refetch } = useFetcher( - (_callApmApi) => { - if (canSave) { - return _callApmApi(`GET /internal/apm/settings/apm-index-settings`); - } - }, - [canSave] - ); + const { data = INITIAL_STATE, refetch } = useFetcher((_callApmApi) => { + return _callApmApi(`GET /internal/apm/settings/apm-index-settings`); + }, []); const { data: space } = useFetcher(() => { return services.spaces?.getActiveSpace(); diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index d2197df31857..09681f01da2d 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -16,6 +16,7 @@ import { ApmRuleType, APM_SERVER_FEATURE_ID, } from '../common/rules/apm_rule_types'; +import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '../common/apm_saved_object_constants'; export const APM_FEATURE = { id: APM_SERVER_FEATURE_ID, @@ -38,7 +39,7 @@ export const APM_FEATURE = { catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], - read: [], + read: [APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE], }, alerting: { alert: { @@ -59,7 +60,7 @@ export const APM_FEATURE = { catalogue: [APM_SERVER_FEATURE_ID], savedObject: { all: [], - read: [], + read: [APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE], }, alerting: { alert: { From 509c46a6f5258a3b08d34ac75ed88244d9e02708 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Thu, 2 Feb 2023 15:08:14 +0100 Subject: [PATCH 09/35] [Defend workflows] Small fixes to osquery (#149305) --- x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts | 2 +- x-pack/plugins/osquery/public/results/results_table.tsx | 5 ++++- .../osquery/osquery_investigation_guide_panel.tsx | 8 ++++++-- .../rule_response_actions/response_actions_list.tsx | 5 ++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts index d098c2bc57fb..290aae563c37 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/alerts.cy.ts @@ -200,7 +200,7 @@ describe('Alert Event Details', () => { it('should be able to add investigation guides to response actions', () => { const investigationGuideNote = - 'It seems that you have suggested queries in investigation guide, would you like to add them as response actions?'; + 'You have queries in the investigation guide. Add them as response actions?'; cy.visit('/app/security/rules'); cy.contains(RULE_NAME).click(); cy.contains('Edit rule settings').click(); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index 53ce6f8ee5ad..3f1ca20f38ea 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -49,7 +49,10 @@ const DataContext = createContext([]); const StyledEuiDataGrid = styled(EuiDataGrid)` :not(.euiDataGrid--fullScreen) { - max-height: 500px; + .euiDataGrid__virtualized { + height: 100% !important; + max-height: 500px; + } } `; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx index 697605dbc5e5..77146ec2fc5a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/osquery/osquery_investigation_guide_panel.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useState } from 'react'; interface OsqueryInvestigationGuidePanelProps { onClick: () => void; + queriesLength: number; } const panelCss = { @@ -19,7 +20,7 @@ const panelCss = { const flexGroupCss = { padding: `0 24px` }; export const OsqueryInvestigationGuidePanel = React.memo( - ({ onClick }) => { + ({ onClick, queriesLength }) => { const [hideInvestigationGuideSuggestion, setHideInvestigationGuideSuggestion] = useState(false); const handleClick = useCallback(() => { @@ -37,7 +38,10 @@ export const OsqueryInvestigationGuidePanel = React.memo diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx index 0f0409c5deb6..44c49529b439 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions/response_actions_list.tsx @@ -54,7 +54,10 @@ export const ResponseActionsList = React.memo(({ items })} {osqueryNoteQueries.length ? ( - + ) : null} ); From 9e2dfb9818235b91107fbf4144f5b83e3b852bc3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 2 Feb 2023 14:24:28 +0000 Subject: [PATCH 10/35] skip flaky suites (#150143,#150144,#150145,#150146) --- x-pack/test/functional/apps/infra/hosts_view.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 1fce5db372eb..9975a2772a19 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -147,7 +147,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('enables hosts view page and checks content', () => { + // FLAKY: https://github.com/elastic/kibana/issues/150143 + // FLAKY: https://github.com/elastic/kibana/issues/150144 + // FLAKY: https://github.com/elastic/kibana/issues/150145 + // FLAKY: https://github.com/elastic/kibana/issues/150146 + describe.skip('enables hosts view page and checks content', () => { before(async () => { await navigateAndEnableHostView(); await pageObjects.timePicker.setAbsoluteRange( From 05d3c9cf7f04f1af1bf999fef12658713faaacf9 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Thu, 2 Feb 2023 15:51:37 +0100 Subject: [PATCH 11/35] [Enterprise search] Promote ingest pipelines in mappings (#150170) ## Summary This adds an info box to promote pipelines/mapping updates on the mappings page. Screenshot 2023-02-02 at 14 33 14 --- .../search_index/index_mappings.tsx | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx index 9160cb39842c..ab27f9536bb0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx @@ -14,6 +14,7 @@ import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiLink, EuiPanel, EuiSpacer, @@ -49,13 +50,20 @@ export const SearchIndexIndexMappings: React.FC = () => { - -

- {i18n.translate('xpack.enterpriseSearch.content.searchIndex.mappings.title', { - defaultMessage: 'About index mappings', - })} -

- + + + + + + +

+ {i18n.translate('xpack.enterpriseSearch.content.searchIndex.mappings.title', { + defaultMessage: 'About index mappings', + })} +

+
+
+

@@ -77,6 +85,39 @@ export const SearchIndexIndexMappings: React.FC = () => { })} + + + + + + + + +

+ {i18n.translate('xpack.enterpriseSearch.content.searchIndex.transform.title', { + defaultMessage: 'Transform your searchable content', + })} +

+ + + + + + +

+ +

+
+ + + {i18n.translate('xpack.enterpriseSearch.content.searchIndex.transform.docLink', { + defaultMessage: 'Learn more', + })} + + From 3c14f008ad09edf612026288ec887abc48444a0f Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 2 Feb 2023 16:02:51 +0100 Subject: [PATCH 12/35] [Security Solution] Avoid exporting execution_summary field (#150097) ## Summary It fixes a problem of exporting `execution_summary` field while exporting detection rules which was introduce in https://github.com/elastic/kibana/pull/147035. Presence of that field make importing of just exported rule failing. Tests to cover this fix will come in a separate PR. --- .../logic/export/get_export_by_object_ids.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts index 60a7ec4c8185..e15ab98e0d0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.ts @@ -118,9 +118,14 @@ export const getRulesFromObjects = async ( isAlertType(matchingRule) && matchingRule.params.immutable !== true ) { + const rule = internalRuleToAPIResponse(matchingRule, legacyActions[matchingRule.id]); + + // Fields containing runtime information shouldn't be exported. It causes import failures. + delete rule.execution_summary; + return { statusCode: 200, - rule: internalRuleToAPIResponse(matchingRule, legacyActions[matchingRule.id]), + rule, }; } else { return { From 9cfec581c803a16209b8645141ee96fb55973361 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Thu, 2 Feb 2023 15:06:06 +0000 Subject: [PATCH 13/35] [SecuritySolution] Render histograms with Lens (#147261) ## Summary Relevant issue: https://github.com/elastic/kibana/issues/136409 These are all behind feature flag `chartEmbeddablesEnabled` --- ### Changes: 1. Legends are all moved to the left side of the chart to avoid overlapping with chart actions. 2. The second `group by` of Alerts Trend Chart is removed (as it is always disabled). --- ### Events: Screenshot 2023-01-25 at 15 33 27 --- ### Top N Screenshot 2023-01-25 at 15 34 21 --- ### No indices: Screenshot 2022-12-21 at 17 03 05 --- ### Alerts - Trend Screenshot 2023-01-25 at 15 34 52 --- ### ~Alerts - Treemap~ (Not included in this PR - https://github.com/elastic/kibana/issues/149592) **Big Difference after converting to Lens. Likely to have a redesign:** Known issues: 1. Alerts tree map: Is not rendered exactly the same due to the limits of dimension of Lens. 3. No value display in each legend item for alerts tree map 4. Background color cannot be decided by risk score Before: Screenshot 2023-01-10 at 12 00 51 After: Screenshot 2023-01-25 at 15 35 16 --- ### ~Alerts - Charts~ (Not included in this PR - https://github.com/elastic/kibana/issues/149592) **Lens does not support the [design](https://github.com/elastic/security-team/issues/5599). Likely to have a redesign:** Screenshot 2023-01-25 at 15 35 55 --- ### Alerts - Table Screenshot 2022-12-21 at 17 12 25 Known issues: 1. https://github.com/elastic/kibana/issues/149828 2. Unable to restore a column after hiding it. 3. https://github.com/elastic/kibana/issues/150048 4. https://github.com/elastic/kibana/issues/150158 --- ### Alerts - Preview Known issue: There's no legend in alerts preview as its legend action, filter in, filter out are not useful on rule creation page. - https://github.com/elastic/kibana/issues/149220 Screenshot 2023-01-26 at 13 32 00 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../show_top_n/show_top_n_component.test.tsx | 1 + .../security_solution/public/app/index.tsx | 5 +- .../common/components/charts/donutchart.tsx | 14 +- .../events_tab/events_query_tab_body.test.tsx | 3 + .../matrix_histogram/chart_content.tsx | 29 + .../matrix_histogram/index.test.tsx | 64 +-- .../components/matrix_histogram/index.tsx | 51 +- .../page/__mocks__/use_refetch_by_session.tsx | 18 + .../page/use_refetch_by_session.tsx | 12 +- .../__snapshots__/authentication.test.ts.snap | 2 +- .../common/__snapshots__/event.test.ts.snap | 3 +- .../__snapshots__/external_alert.test.ts.snap | 2 +- .../alerts_by_status_donut.test.ts.snap | 166 ++++++ .../alerts_histogram.test.ts.snap | 343 ++++++++++++ .../__snapshots__/alerts_table.test.ts.snap | 523 ++++++++++++++++++ .../__snapshots__/rule_preview.test.ts.snap | 173 ++++++ .../alerts/alerts_by_status_donut.test.ts | 63 +++ .../common/alerts/alerts_by_status_donut.ts | 61 +- .../common/alerts/alerts_histogram.test.ts | 88 +++ .../common/alerts/alerts_histogram.ts | 127 +++++ .../common/alerts/alerts_table.test.ts | 100 ++++ .../common/alerts/alerts_table.ts | 137 +++++ .../common/alerts/rule_preview.test.ts | 70 +++ .../common/alerts/rule_preview.ts | 167 ++++++ .../lens_attributes/common/authentication.ts | 2 +- .../lens_attributes/common/event.test.ts | 4 + .../lens_attributes/common/events.ts | 20 +- .../lens_attributes/common/external_alert.ts | 2 +- .../dns_top_domains.test.ts.snap | 2 +- .../network/dns_top_domains.ts | 2 +- .../visualization_actions/lens_embeddable.tsx | 20 +- .../visualization_actions/mocks.tsx | 47 ++ .../components/visualization_actions/types.ts | 1 + .../visualization_actions/use_actions.test.ts | 12 +- .../visualization_actions/use_actions.ts | 10 +- .../use_lens_attributes.test.tsx | 39 ++ .../use_lens_attributes.tsx | 9 +- .../visualization_embeddable.test.tsx | 93 +++- .../visualization_embeddable.tsx | 78 ++- .../containers/matrix_histogram/index.ts | 5 +- .../alerts_count_panel/alerts_count.tsx | 2 +- .../alerts_count_panel/chart_content.tsx | 61 ++ .../alerts_count_panel/index.test.tsx | 69 ++- .../alerts_kpis/alerts_count_panel/index.tsx | 64 ++- .../alerts_histogram_panel/index.test.tsx | 49 +- .../alerts_histogram_panel/index.tsx | 72 ++- .../components/alerts_kpis/common/hooks.ts | 18 +- .../rules/rule_preview/preview_histogram.tsx | 52 +- .../rule_preview/use_preview_histogram.tsx | 6 +- .../chart_panels/index.test.tsx | 13 +- .../detection_engine/chart_panels/index.tsx | 56 +- .../detection_engine.test.tsx | 3 + .../authentications_query_tab_body.test.tsx | 6 +- .../security_solution/public/helpers.tsx | 10 - .../alerts_by_status/alerts_by_status.tsx | 3 - 55 files changed, 2782 insertions(+), 270 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/page/__mocks__/use_refetch_by_session.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx diff --git a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx index 4edbc9fe85a3..6c702bb20fd9 100644 --- a/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx +++ b/x-pack/plugins/security_solution/public/actions/show_top_n/show_top_n_component.test.tsx @@ -20,6 +20,7 @@ jest.mock('react-router-dom', () => { useLocation: jest.fn().mockReturnValue({ pathname: '/test' }), }; }); +jest.mock('../../common/components/visualization_actions'); const casesService = { ui: { getCasesContext: () => mockCasesContext }, diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 98b82a8d5b8f..29abcc5c475e 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -49,5 +49,8 @@ export const renderApp = ({ , element ); - return () => unmountComponentAtNode(element); + return () => { + services.data.search.session.clear(); + unmountComponentAtNode(element); + }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx index 1358739742b6..8b968077f3dc 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/donutchart.tsx @@ -46,7 +46,6 @@ export interface DonutChartProps { data: DonutChartData[] | null | undefined; fillColor: FillColor; height?: number; - isChartEmbeddablesEnabled?: boolean; label: React.ReactElement | string; legendItems?: LegendItem[] | null | undefined; onElementClick?: ElementClickListener; @@ -67,10 +66,10 @@ export interface DonutChartWrapperProps { /* Make this position absolute in order to overlap the text onto the donut */ export const DonutTextWrapper = styled(EuiFlexGroup)< EuiFlexGroupProps & { - $isChartEmbeddablesEnabled?: boolean; $dataExists?: boolean; + $donutTextWrapperStyles?: FlattenSimpleInterpolation; + $isChartEmbeddablesEnabled?: boolean; className?: string; - donutTextWrapperStyles?: FlattenSimpleInterpolation; } >` top: ${({ $isChartEmbeddablesEnabled, $dataExists }) => @@ -80,8 +79,8 @@ export const DonutTextWrapper = styled(EuiFlexGroup)< position: absolute; z-index: 1; - ${({ className, donutTextWrapperStyles }) => - className && donutTextWrapperStyles ? `&.${className} {${donutTextWrapperStyles}}` : ''} + ${({ className, $donutTextWrapperStyles }) => + className && $donutTextWrapperStyles ? `&.${className} {${$donutTextWrapperStyles}}` : ''} `; export const StyledEuiFlexItem = styled(EuiFlexItem)` @@ -117,11 +116,11 @@ const DonutChartWrapperComponent: React.FC = ({ @@ -151,7 +150,6 @@ export const DonutChart = ({ data, fillColor, height = 90, - isChartEmbeddablesEnabled, label, legendItems, onElementClick, @@ -165,7 +163,7 @@ export const DonutChart = ({ dataExists={data != null && data.length > 0} label={label} title={title} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} + isChartEmbeddablesEnabled={false} > <> {data == null || totalCount == null || totalCount === 0 ? ( diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index c06390b4f9a1..1387784d8286 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -47,6 +47,9 @@ jest.mock('../../lib/kibana', () => { }; }); +jest.mock('../visualization_actions'); +jest.mock('../visualization_actions/lens_embeddable'); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => mockHistory, diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx new file mode 100644 index 000000000000..65d3774af1ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/chart_content.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { BarChartComponentProps } from '../charts/barchart'; +import { BarChart } from '../charts/barchart'; +import { MatrixLoader } from './matrix_loader'; + +const MatrixHistogramChartContentComponent = ({ + isInitialLoading, + barChart, + configs, + stackByField, + scopeId, +}: BarChartComponentProps & { isInitialLoading: boolean }) => { + return isInitialLoading ? ( + + ) : ( + + ); +}; + +export const MatrixHistogramChartContent = React.memo(MatrixHistogramChartContentComponent); + +MatrixHistogramChartContentComponent.displayName = 'MatrixHistogramChartContentComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index ac5a43d23a28..ec4a25039e91 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -176,22 +176,7 @@ describe('Matrix Histogram Component', () => { }); describe('Inspect button', () => { - test("it doesn't render Inspect button by default on Host page", () => { - mockLocation.mockReturnValue({ pathname: '/hosts' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(false); - }); - - test("it doesn't render Inspect button by default on Network page", () => { - mockLocation.mockReturnValue({ pathname: '/network' }); - + test("it doesn't render Inspect button by default", () => { const testProps = { ...mockMatrixOverTimeHistogramProps, lensAttributes: dnsTopDomainsLensAttributes, @@ -201,41 +186,10 @@ describe('Matrix Histogram Component', () => { }); expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(false); }); - - test('it render Inspect button by default on other pages', () => { - mockLocation.mockReturnValue({ pathname: '/overview' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(true); - }); }); describe('VisualizationActions', () => { - test('it renders VisualizationActions on Host page if lensAttributes is provided', () => { - mockLocation.mockReturnValue({ pathname: '/hosts' }); - - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(true); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').prop('className')).toEqual( - 'histogram-viz-actions' - ); - }); - - test('it renders VisualizationActions on Network page if lensAttributes is provided', () => { - mockLocation.mockReturnValue({ pathname: '/network' }); - + test('it renders VisualizationActions if lensAttributes is provided', () => { const testProps = { ...mockMatrixOverTimeHistogramProps, lensAttributes: dnsTopDomainsLensAttributes, @@ -248,20 +202,6 @@ describe('Matrix Histogram Component', () => { 'histogram-viz-actions' ); }); - - test("it doesn't renders VisualizationActions except Host / Network pages", () => { - const testProps = { - ...mockMatrixOverTimeHistogramProps, - lensAttributes: dnsTopDomainsLensAttributes, - }; - - mockLocation.mockReturnValue({ pathname: '/overview' }); - - wrapper = mount(, { - wrappingComponent: TestProviders, - }); - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(false); - }); }); describe('toggle query', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 7b44b9295218..48e812ff2afa 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -11,11 +11,8 @@ import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; import { useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import * as i18n from './translations'; -import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; -import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; import { getBarchartConfigs, getCustomChartData } from './utils'; import { useMatrixHistogramCombined } from '../../containers/matrix_histogram'; @@ -35,8 +32,10 @@ import { HoverVisibilityContainer } from '../hover_visibility_container'; import { VisualizationActions } from '../visualization_actions'; import type { GetLensAttributes, LensAttributes } from '../visualization_actions/types'; import { useQueryToggle } from '../../containers/query_toggle'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils'; -import { isExplorePage } from '../../../helpers'; +import { VisualizationEmbeddable } from '../visualization_actions/visualization_embeddable'; +import { MatrixHistogramChartContent } from './chart_content'; export type MatrixHistogramComponentProps = MatrixHistogramProps & Omit & { @@ -71,6 +70,8 @@ const HistogramPanel = styled(Panel)<{ height?: number }>` ${({ height }) => (height != null ? `min-height: ${height}px;` : '')} `; +const CHART_HEIGHT = '150px'; + export const MatrixHistogramComponent: React.FC = ({ chartHeight, defaultStackByOption, @@ -107,7 +108,6 @@ export const MatrixHistogramComponent: React.FC = hideQueryToggle = false, }) => { const dispatch = useDispatch(); - const { pathname } = useLocation(); const handleBrushEnd = useCallback( ({ x }) => { @@ -169,6 +169,8 @@ export const MatrixHistogramComponent: React.FC = [setQuerySkip, setToggleStatus] ); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const matrixHistogramRequest = { endDate, errorMessage, @@ -180,11 +182,10 @@ export const MatrixHistogramComponent: React.FC = stackByField: selectedStackByOption.value, runtimeMappings, isPtrIncluded, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, }; const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined(matrixHistogramRequest); - const onExplorePage = isExplorePage(pathname); const titleWithStackByField = useMemo( () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), @@ -209,22 +210,28 @@ export const MatrixHistogramComponent: React.FC = useEffect(() => { if (!loading && !isInitialLoading) { - setQuery({ id, inspect, loading, refetch }); + setQuery({ + id, + inspect, + loading, + refetch, + }); } if (isInitialLoading && !!barChartData && data) { setIsInitialLoading(false); } }, [ - setQuery, + barChartData, + data, id, inspect, + isChartEmbeddablesEnabled, + isInitialLoading, loading, refetch, - isInitialLoading, - barChartData, - data, setIsInitialLoading, + setQuery, ]); const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]); @@ -261,11 +268,11 @@ export const MatrixHistogramComponent: React.FC = toggleQuery={hideQueryToggle ? undefined : toggleQuery} subtitle={subtitleWithCounts} inspectMultiple - showInspectButton={showInspectButton || !onExplorePage} + showInspectButton={showInspectButton && !isChartEmbeddablesEnabled} isInspectDisabled={filterQuery === undefined} > - {onExplorePage && (getLensAttributes || lensAttributes) && timerange && ( + {(getLensAttributes || lensAttributes) && timerange && ( = {toggleStatus ? ( - isInitialLoading ? ( - + isChartEmbeddablesEnabled ? ( + ) : ( - ; refetchByRestartingSession: Refetch; + refetchByDeletingSession: Refetch; } => { const dispatch = useDispatch(); const { data } = useKibana().services; @@ -44,6 +45,7 @@ export const useRefetchByRestartingSession = ({ ); const refetchByRestartingSession = useCallback(() => { + const searchSessionId = session.current.start(); dispatch( inputsActions.setInspectionParameter({ id: queryId, @@ -54,13 +56,21 @@ export const useRefetchByRestartingSession = ({ * like most of our components, it refetches when receiving a new search * session ID. **/ - searchSessionId: skip ? undefined : session.current.start(), + searchSessionId: skip ? undefined : searchSessionId, }) ); }, [dispatch, queryId, selectedInspectIndex, skip]); + /** + * This is for refetching alert index when the first rule just created + */ + const refetchByDeletingSession = useCallback(() => { + dispatch(inputsActions.deleteOneQuery({ inputId: InputsModelId.global, id: queryId })); + }, [dispatch, queryId]); + return { session, refetchByRestartingSession, + refetchByDeletingSession, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap index 84d29e3e7fd9..a00e6517914d 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/authentication.test.ts.snap @@ -251,7 +251,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap index 93bdbe2a0dca..ab1eed774e8a 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/event.test.ts.snap @@ -175,7 +175,8 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "legendSize": "xlarge", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap index d53499bd57c8..7d06e63032e9 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/__snapshots__/external_alert.test.ts.snap @@ -206,7 +206,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar_stacked", "title": "Empty XY chart", diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap new file mode 100644 index 000000000000..12de83c43fc3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_by_status_donut.test.ts.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsByStatusAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-b9b43606-7ff7-46ae-a47c-85bed80fab9a", + "type": "index-pattern", + }, + Object { + "id": "security-solution-my-test", + "name": "a1aaa83b-5026-444e-9465-50e0afade01c", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "b9b43606-7ff7-46ae-a47c-85bed80fab9a": Object { + "columnOrder": Array [ + "a9b43606-7ff7-46ae-a47c-85bed80fab9a", + "21cc4a49-3780-4b1a-be28-f02fa5303d24", + ], + "columns": Object { + "21cc4a49-3780-4b1a-be28-f02fa5303d24": Object { + "dataType": "number", + "filter": Object { + "language": "kuery", + "query": "", + }, + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "a9b43606-7ff7-46ae-a47c-85bed80fab9a": Object { + "dataType": "string", + "isBucketed": true, + "label": "Filters", + "operationType": "filters", + "params": Object { + "filters": Array [ + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity: \\"critical\\"", + }, + "label": "Critical", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity : \\"high\\" ", + }, + "label": "High", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity: \\"medium\\"", + }, + "label": "Medium", + }, + Object { + "input": Object { + "language": "kuery", + "query": "kibana.alert.severity : \\"low\\" ", + }, + "label": "Low", + }, + ], + }, + "scale": "ordinal", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "layers": Array [ + Object { + "categoryDisplay": "hide", + "emptySizeRatio": 0.85, + "layerId": "b9b43606-7ff7-46ae-a47c-85bed80fab9a", + "layerType": "data", + "legendDisplay": "hide", + "metrics": Array [ + "21cc4a49-3780-4b1a-be28-f02fa5303d24", + ], + "nestedLegend": true, + "numberDisplay": "value", + "percentDecimals": 2, + "primaryGroups": Array [ + "a9b43606-7ff7-46ae-a47c-85bed80fab9a", + ], + }, + ], + "shape": "donut", + }, + }, + "title": "Alerts", + "visualizationType": "lnsPie", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap new file mode 100644 index 000000000000..d0b6f7a79ce3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_histogram.test.ts.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsHistogramLensAttributes should render with extra options - filters 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "0039eb0c-9a1a-4687-ae54-0f4e239bec75": Object { + "columnOrder": Array [ + "34919782-4546-43a5-b668-06ac934d3acd", + "aac9d7d0-13a3-480a-892b-08207a787926", + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "columns": Object { + "34919782-4546-43a5-b668-06ac934d3acd": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "e09e0380-0740-4105-becc-0a4ca12e3944", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "secondaryFields": Array [], + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "aac9d7d0-13a3-480a-892b-08207a787926": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e09e0380-0740-4105-becc-0a4ca12e3944": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + ".alerts-security.alerts-default", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": ".alerts-security.alerts-default", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "layerId": "0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "34919782-4546-43a5-b668-06ac934d3acd", + "xAccessor": "aac9d7d0-13a3-480a-892b-08207a787926", + }, + ], + "legend": Object { + "isVisible": true, + "legendSize": "xlarge", + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Alerts", + "visualizationType": "lnsXY", +} +`; + +exports[`getAlertsHistogramLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "0039eb0c-9a1a-4687-ae54-0f4e239bec75": Object { + "columnOrder": Array [ + "34919782-4546-43a5-b668-06ac934d3acd", + "aac9d7d0-13a3-480a-892b-08207a787926", + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "columns": Object { + "34919782-4546-43a5-b668-06ac934d3acd": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "missingBucket": false, + "orderBy": Object { + "columnId": "e09e0380-0740-4105-becc-0a4ca12e3944", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "secondaryFields": Array [], + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "aac9d7d0-13a3-480a-892b-08207a787926": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + "e09e0380-0740-4105-becc-0a4ca12e3944": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "scale": "ratio", + "sourceField": "___records___", + }, + }, + "incompleteColumns": Object {}, + }, + }, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "e09e0380-0740-4105-becc-0a4ca12e3944", + ], + "layerId": "0039eb0c-9a1a-4687-ae54-0f4e239bec75", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "34919782-4546-43a5-b668-06ac934d3acd", + "xAccessor": "aac9d7d0-13a3-480a-892b-08207a787926", + }, + ], + "legend": Object { + "isVisible": true, + "legendSize": "xlarge", + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yLeftExtent": Object { + "mode": "full", + }, + "yRightExtent": Object { + "mode": "full", + }, + }, + }, + "title": "Alerts", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap new file mode 100644 index 000000000000..58ecf5d44d01 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/alerts_table.test.ts.snap @@ -0,0 +1,523 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertsTableLensAttributes should render with extra options - breakdownField 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of agent.type", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "agent.type", + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of agent.type", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "agent.type", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; + +exports[`getAlertsTableLensAttributes should render with extra options - filters 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of undefined", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": undefined, + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of undefined", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": undefined, + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + ".alerts-security.alerts-default", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": ".alerts-security.alerts-default", + }, + }, + ], + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; + +exports[`getAlertsTableLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [ + Object { + "id": "security-solution-my-test", + "name": "indexpattern-datasource-layer-4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "type": "index-pattern", + }, + ], + "state": Object { + "adHocDataViews": Object {}, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b": Object { + "columnOrder": Array [ + "2881fedd-54b7-42ba-8c97-5175dec86166", + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "f04a71a3-399f-4d32-9efc-8a005e989991", + ], + "columns": Object { + "2881fedd-54b7-42ba-8c97-5175dec86166": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "75ce269b-ee9c-4c7d-a14e-9226ba0fe059": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top values of undefined", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 1000, + }, + "scale": "ordinal", + "sourceField": undefined, + }, + "f04a71a3-399f-4d32-9efc-8a005e989991": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of undefined", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": undefined, + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "_index", + "negate": false, + "params": Array [ + "signal-index", + ], + "type": "phrases", + }, + "query": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "_index": "signal-index", + }, + }, + ], + }, + }, + }, + ], + "internalReferences": Array [], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "columns": Array [ + Object { + "columnId": "2881fedd-54b7-42ba-8c97-5175dec86166", + "isTransposed": false, + "width": 362, + }, + Object { + "columnId": "f04a71a3-399f-4d32-9efc-8a005e989991", + "isTransposed": false, + }, + Object { + "columnId": "75ce269b-ee9c-4c7d-a14e-9226ba0fe059", + "isTransposed": false, + }, + ], + "layerId": "4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b", + "layerType": "data", + }, + }, + "title": "Alerts", + "visualizationType": "lnsDatatable", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap new file mode 100644 index 000000000000..5a841f2bc942 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/__snapshots__/rule_preview.test.ts.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getRulePreviewLensAttributes should render without extra options 1`] = ` +Object { + "description": "", + "references": Array [], + "state": Object { + "adHocDataViews": Object { + "mockInternalReferenceId": Object { + "allowNoIndex": false, + "fieldAttrs": Object {}, + "fieldFormats": Object {}, + "id": "mockInternalReferenceId", + "name": ".preview.alerts-security.alerts-undefined", + "runtimeFieldMap": Object {}, + "sourceFilters": Array [], + "timeFieldName": "@timestamp", + "title": ".preview.alerts-security.alerts-undefined", + }, + }, + "datasourceStates": Object { + "formBased": Object { + "layers": Object { + "mockLayerId": Object { + "columnOrder": Array [ + "e92c8920-0449-4564-81f4-8945517817a4", + "eba07b4d-766d-49d7-8435-d40367d3d055", + "9c89324b-0c59-4403-9698-d989a09dc5a8", + ], + "columns": Object { + "9c89324b-0c59-4403-9698-d989a09dc5a8": Object { + "dataType": "number", + "isBucketed": false, + "label": "Count of records", + "operationType": "count", + "params": Object { + "emptyAsNull": true, + }, + "scale": "ratio", + "sourceField": "___records___", + }, + "e92c8920-0449-4564-81f4-8945517817a4": Object { + "dataType": "string", + "isBucketed": true, + "label": "Top 10 values of event.category", + "operationType": "terms", + "params": Object { + "exclude": Array [], + "excludeIsRegex": false, + "include": Array [], + "includeIsRegex": false, + "missingBucket": false, + "orderBy": Object { + "columnId": "9c89324b-0c59-4403-9698-d989a09dc5a8", + "type": "column", + }, + "orderDirection": "desc", + "otherBucket": true, + "parentFormat": Object { + "id": "terms", + }, + "size": 10, + }, + "scale": "ordinal", + "sourceField": "event.category", + }, + "eba07b4d-766d-49d7-8435-d40367d3d055": Object { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": Object { + "dropPartials": false, + "includeEmptyRows": true, + "interval": "auto", + }, + "scale": "interval", + "sourceField": "@timestamp", + }, + }, + "incompleteColumns": Object {}, + "sampling": 1, + }, + }, + }, + "textBased": Object { + "layers": Object {}, + }, + }, + "filters": Array [ + Object { + "meta": Object { + "alias": null, + "disabled": false, + "field": "kibana.alert.rule.uuid", + "index": "mockInternalReferenceId", + "key": "kibana.alert.rule.uuid", + "negate": false, + "params": Object { + "query": undefined, + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "kibana.alert.rule.uuid": undefined, + }, + }, + }, + Object { + "meta": Object { + "alias": null, + "disabled": false, + "key": "host.id", + "negate": false, + "params": Object { + "query": "123", + }, + "type": "phrase", + }, + "query": Object { + "match_phrase": Object { + "host.id": "123", + }, + }, + }, + ], + "internalReferences": Array [ + Object { + "id": "mockInternalReferenceId", + "name": "indexpattern-datasource-layer-mockLayerId", + "type": "index-pattern", + }, + ], + "query": Object { + "language": "kql", + "query": "host.name: *", + }, + "visualization": Object { + "axisTitlesVisibilitySettings": Object { + "x": false, + "yLeft": false, + "yRight": true, + }, + "layers": Array [ + Object { + "accessors": Array [ + "9c89324b-0c59-4403-9698-d989a09dc5a8", + ], + "layerId": "mockLayerId", + "layerType": "data", + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "splitAccessor": "e92c8920-0449-4564-81f4-8945517817a4", + "xAccessor": "eba07b4d-766d-49d7-8435-d40367d3d055", + }, + ], + "legend": Object { + "isVisible": false, + "position": "left", + }, + "preferredSeriesType": "bar_stacked", + "title": "Empty XY chart", + "valueLabels": "hide", + "valuesInLegend": true, + "yTitle": "", + }, + }, + "title": "Rule preview", + "visualizationType": "lnsXY", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.ts new file mode 100644 index 000000000000..109bf7da68be --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.test.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 { renderHook } from '@testing-library/react-hooks'; +import { mockExtraFilter, wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsByStatusAttributes } from './alerts_by_status_donut'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('b9b43606-7ff7-46ae-a47c-85bed80fab9a'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getAlertsByStatusAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsByStatusAttributes, + stackByField: 'kibana.alert.workflow_status', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: mockExtraFilter, + }, + getLensAttributes: getAlertsByStatusAttributes, + stackByField: 'kibana.alert.workflow_status', + }), + { wrapper } + ); + + expect(result?.current?.state.filters).toEqual(expect.arrayContaining(mockExtraFilter)); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts index 6875a2c7f6b5..33bc6827fa02 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_by_status_donut.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import type { GetLensAttributes, LensAttributes } from '../../../types'; +import { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; +const layerId = uuidv4(); export const getAlertsByStatusAttributes: GetLensAttributes = ( stackByField = 'kibana.alert.workflow_status', extraOptions -) => - ({ +) => { + return { title: 'Alerts', description: '', visualizationType: 'lnsPie', @@ -20,7 +21,7 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( shape: 'donut', layers: [ { - layerId: '51ed355e-6e23-4038-a417-f653a1160370', + layerId, primaryGroups: ['a9b43606-7ff7-46ae-a47c-85bed80fab9a'], metrics: ['21cc4a49-3780-4b1a-be28-f02fa5303d24'], numberDisplay: 'value', @@ -38,30 +39,35 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( language: 'kuery', }, filters: [ - { - meta: { - disabled: false, - negate: false, - alias: null, - index: 'a1aaa83b-5026-444e-9465-50e0afade01c', - key: stackByField, - field: stackByField, - params: { - query: extraOptions?.status, - }, - type: 'phrase', - }, - query: { - match_phrase: { - [stackByField]: extraOptions?.status, - }, - }, - }, + ...(extraOptions?.status && stackByField + ? [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: 'a1aaa83b-5026-444e-9465-50e0afade01c', + key: stackByField, + field: stackByField, + params: { + query: extraOptions?.status, + }, + type: 'phrase', + }, + query: { + match_phrase: { + [stackByField]: extraOptions?.status, + }, + }, + }, + ] + : []), + ...(extraOptions?.filters ? extraOptions.filters : []), ], datasourceStates: { formBased: { layers: { - '51ed355e-6e23-4038-a417-f653a1160370': { + [layerId]: { columns: { 'a9b43606-7ff7-46ae-a47c-85bed80fab9a': { label: 'Filters', @@ -138,7 +144,7 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( { type: 'index-pattern', id: '{dataViewId}', - name: 'indexpattern-datasource-layer-51ed355e-6e23-4038-a417-f653a1160370', + name: `indexpattern-datasource-layer-${layerId}`, }, { type: 'index-pattern', @@ -146,4 +152,5 @@ export const getAlertsByStatusAttributes: GetLensAttributes = ( id: '{dataViewId}', }, ], - } as LensAttributes); + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts new file mode 100644 index 000000000000..bf84e4999faa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsHistogramLensAttributes } from './alerts_histogram'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + detailName: 'mockRule', + pageName: 'rules', + tabName: 'alerts', + }, + ]), +})); + +describe('getAlertsHistogramLensAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsHistogramLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + ], + }, + getLensAttributes: getAlertsHistogramLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts new file mode 100644 index 000000000000..78b4a134a762 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram.ts @@ -0,0 +1,127 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; +const layerId = uuidv4(); + +export const getAlertsHistogramLensAttributes: GetLensAttributes = ( + stackByField = 'kibana.alert.rule.name', + extraOptions +) => { + return { + title: 'Alerts', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + title: 'Empty XY chart', + legend: { + isVisible: true, + position: 'left', + legendSize: 'xlarge', + }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId, + accessors: ['e09e0380-0740-4105-becc-0a4ca12e3944'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + layerType: 'data', + xAccessor: 'aac9d7d0-13a3-480a-892b-08207a787926', + splitAccessor: '34919782-4546-43a5-b668-06ac934d3acd', + }, + ], + yRightExtent: { + mode: 'full', + }, + yLeftExtent: { + mode: 'full', + }, + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + valuesInLegend: true, + }, + query: { + query: '', + language: 'kuery', + }, + filters: extraOptions?.filters ? extraOptions.filters : [], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + 'aac9d7d0-13a3-480a-892b-08207a787926': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + }, + }, + 'e09e0380-0740-4105-becc-0a4ca12e3944': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + }, + '34919782-4546-43a5-b668-06ac934d3acd': { + label: `Top values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'e09e0380-0740-4105-becc-0a4ca12e3944', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + secondaryFields: [], + }, + }, + }, + columnOrder: [ + '34919782-4546-43a5-b668-06ac934d3acd', + 'aac9d7d0-13a3-480a-892b-08207a787926', + 'e09e0380-0740-4105-becc-0a4ca12e3944', + ], + incompleteColumns: {}, + }, + }, + }, + }, + internalReferences: [], + adHocDataViews: {}, + }, + references: [ + { + type: 'index-pattern', + id: '{dataViewId}', + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts new file mode 100644 index 000000000000..e8457e8dfb53 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getAlertsTableLensAttributes } from './alerts_table'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('4aa7cf71-cf20-4e62-8ca6-ca6be6b0988b'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getAlertsTableLensAttributes', () => { + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + filters: [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + ], + }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - breakdownField', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { breakdownField: 'agent.type' }, + getLensAttributes: getAlertsTableLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts new file mode 100644 index 000000000000..678179855557 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/alerts_table.ts @@ -0,0 +1,137 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; + +const layerId = uuidv4(); + +export const getAlertsTableLensAttributes: GetLensAttributes = ( + stackByField = 'kibana.alert.rule.name', + extraOptions +) => { + return { + title: 'Alerts', + description: '', + visualizationType: 'lnsDatatable', + state: { + visualization: { + columns: [ + { + columnId: '2881fedd-54b7-42ba-8c97-5175dec86166', + isTransposed: false, + width: 362, + }, + { + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + isTransposed: false, + }, + { + columnId: '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', + isTransposed: false, + }, + ], + layerId, + layerType: 'data', + }, + query: { + query: '', + language: 'kuery', + }, + filters: extraOptions?.filters ? extraOptions.filters : [], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + '2881fedd-54b7-42ba-8c97-5175dec86166': { + label: `Top values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + 'f04a71a3-399f-4d32-9efc-8a005e989991': { + label: `Count of ${extraOptions?.breakdownField}`, + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: extraOptions?.breakdownField, + params: { + emptyAsNull: true, + }, + }, + '75ce269b-ee9c-4c7d-a14e-9226ba0fe059': { + label: `Top values of ${extraOptions?.breakdownField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: extraOptions?.breakdownField, + isBucketed: true, + params: { + size: 1000, + orderBy: { + type: 'column', + columnId: 'f04a71a3-399f-4d32-9efc-8a005e989991', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + }, + columnOrder: [ + '2881fedd-54b7-42ba-8c97-5175dec86166', + '75ce269b-ee9c-4c7d-a14e-9226ba0fe059', + 'f04a71a3-399f-4d32-9efc-8a005e989991', + ], + sampling: 1, + incompleteColumns: {}, + }, + }, + }, + textBased: { + layers: {}, + }, + }, + internalReferences: [], + adHocDataViews: {}, + }, + references: [ + { + type: 'index-pattern', + id: '{dataViewId}', + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts new file mode 100644 index 000000000000..85b4a11bbc7f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { mockRulePreviewFilter, wrapper } from '../../../mocks'; + +import { useLensAttributes } from '../../../use_lens_attributes'; + +import { getRulePreviewLensAttributes } from './rule_preview'; +const mockInternalReferenceId = 'mockInternalReferenceId'; +const mockRuleId = 'mockRuleId'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValueOnce('mockLayerId').mockReturnValueOnce('mockInternalReferenceId'), +})); + +jest.mock('../../../../../containers/sourcerer', () => ({ + useSourcererDataView: jest.fn().mockReturnValue({ + dataViewId: 'security-solution-my-test', + indicesExist: true, + selectedPatterns: ['signal-index'], + }), +})); + +jest.mock('../../../../../utils/route/use_route_spy', () => ({ + useRouteSpy: jest.fn().mockReturnValue([ + { + pageName: 'alerts', + }, + ]), +})); + +describe('getRulePreviewLensAttributes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should render without extra options', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getRulePreviewLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current).toMatchSnapshot(); + }); + + it('should render with extra options - filters', () => { + const { result } = renderHook( + () => + useLensAttributes({ + extraOptions: { + ruleId: mockRuleId, + }, + getLensAttributes: getRulePreviewLensAttributes, + stackByField: 'event.category', + }), + { wrapper } + ); + + expect(result?.current?.state.filters).toEqual( + expect.arrayContaining(mockRulePreviewFilter(mockInternalReferenceId, mockRuleId)) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts new file mode 100644 index 000000000000..33d59c358ea5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/alerts/rule_preview.ts @@ -0,0 +1,167 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../../types'; + +const layerId = uuidv4(); +const internalReferenceId = uuidv4(); + +export const getRulePreviewLensAttributes: GetLensAttributes = ( + stackByField = 'event.category', + extraOptions +) => { + return { + title: 'Rule preview', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + title: 'Empty XY chart', + legend: { + isVisible: false, + position: 'left', + }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId, + accessors: ['9c89324b-0c59-4403-9698-d989a09dc5a8'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + layerType: 'data', + xAccessor: 'eba07b4d-766d-49d7-8435-d40367d3d055', + splitAccessor: 'e92c8920-0449-4564-81f4-8945517817a4', + }, + ], + valuesInLegend: true, + yTitle: '', + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + }, + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: internalReferenceId, + key: 'kibana.alert.rule.uuid', + field: 'kibana.alert.rule.uuid', + params: { + query: extraOptions?.ruleId, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'kibana.alert.rule.uuid': extraOptions?.ruleId, + }, + }, + }, + ], + datasourceStates: { + formBased: { + layers: { + [layerId]: { + columns: { + '9c89324b-0c59-4403-9698-d989a09dc5a8': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: '___records___', + params: { + emptyAsNull: true, + }, + }, + 'eba07b4d-766d-49d7-8435-d40367d3d055': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + includeEmptyRows: true, + dropPartials: false, + }, + }, + 'e92c8920-0449-4564-81f4-8945517817a4': { + label: `Top 10 values of ${stackByField}`, + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: stackByField, + isBucketed: true, + params: { + size: 10, + orderBy: { + type: 'column', + columnId: '9c89324b-0c59-4403-9698-d989a09dc5a8', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + parentFormat: { + id: 'terms', + }, + include: [], + exclude: [], + includeIsRegex: false, + excludeIsRegex: false, + }, + }, + }, + columnOrder: [ + 'e92c8920-0449-4564-81f4-8945517817a4', + 'eba07b4d-766d-49d7-8435-d40367d3d055', + '9c89324b-0c59-4403-9698-d989a09dc5a8', + ], + sampling: 1, + incompleteColumns: {}, + }, + }, + }, + textBased: { + layers: {}, + }, + }, + internalReferences: [ + { + type: 'index-pattern', + id: internalReferenceId, + name: `indexpattern-datasource-layer-${layerId}`, + }, + ], + adHocDataViews: { + [internalReferenceId]: { + id: internalReferenceId, + title: `.preview.alerts-security.alerts-${extraOptions?.spaceId}`, + timeFieldName: '@timestamp', + sourceFilters: [], + fieldFormats: {}, + runtimeFieldMap: {}, + fieldAttrs: {}, + allowNoIndex: false, + name: `.preview.alerts-security.alerts-${extraOptions?.spaceId}`, + }, + }, + }, + references: [], + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts index 4e69bac6287e..4378b74400aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/authentication.ts @@ -20,7 +20,7 @@ export const authenticationLensAttributes: LensAttributes = { title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts index 87d246fc2350..a9a1c3951de5 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/event.test.ts @@ -12,6 +12,10 @@ import { useLensAttributes } from '../../use_lens_attributes'; import { getEventsHistogramLensAttributes } from './events'; +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'), +})); + jest.mock('../../../../containers/sourcerer', () => ({ useSourcererDataView: jest.fn().mockReturnValue({ selectedPatterns: ['auditbeat-mytest-*'], diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts index e48f6aa6c1a8..61e9bac0cb3a 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/events.ts @@ -4,13 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { v4 as uuidv4 } from 'uuid'; +import type { GetLensAttributes } from '../../types'; -import type { GetLensAttributes, LensAttributes } from '../../types'; +const layerId = uuidv4(); export const getEventsHistogramLensAttributes: GetLensAttributes = ( stackByField = 'event.action' -) => - ({ +) => { + return { title: 'Events', description: '', visualizationType: 'lnsXY', @@ -19,13 +21,14 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', + legendSize: 'xlarge', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', layers: [ { - layerId: '0039eb0c-9a1a-4687-ae54-0f4e239bec75', + layerId, accessors: ['e09e0380-0740-4105-becc-0a4ca12e3944'], position: 'top', seriesType: 'bar_stacked', @@ -55,7 +58,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( datasourceStates: { formBased: { layers: { - '0039eb0c-9a1a-4687-ae54-0f4e239bec75': { + [layerId]: { columns: { 'aac9d7d0-13a3-480a-892b-08207a787926': { label: '@timestamp', @@ -113,7 +116,8 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = ( { type: 'index-pattern', id: '{dataViewId}', - name: 'indexpattern-datasource-layer-0039eb0c-9a1a-4687-ae54-0f4e239bec75', + name: `indexpattern-datasource-layer-${layerId}`, }, ], - } as LensAttributes); + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts index f5a664b98161..44aa790332ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/external_alert.ts @@ -20,7 +20,7 @@ export const getExternalAlertLensAttributes: GetLensAttributes = ( title: 'Empty XY chart', legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', preferredSeriesType: 'bar_stacked', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap index 38ee4c908fce..a261abe99ffc 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/__snapshots__/dns_top_domains.test.ts.snap @@ -231,7 +231,7 @@ Object { ], "legend": Object { "isVisible": true, - "position": "right", + "position": "left", }, "preferredSeriesType": "bar", "tickLabelsVisibilitySettings": Object { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts index 0f195bdeaa8d..2e9ff9226151 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/network/dns_top_domains.ts @@ -17,7 +17,7 @@ export const dnsTopDomainsLensAttributes: LensAttributes = { visualization: { legend: { isVisible: true, - position: 'right', + position: 'left', }, valueLabels: 'hide', fittingFunction: 'None', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx index 985a9881d330..19805b8ce96f 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_embeddable.tsx @@ -31,7 +31,7 @@ const LensComponentWrapper = styled.div<{ height?: string; width?: string }>` background-color: transparent; } .expExpressionRenderer__expression { - padding: 0 !important; + padding: 2px 0 0 0 !important; } .legacyMtrVis__container { padding: 0; @@ -48,8 +48,6 @@ const initVisualizationData: { isLoading: true, }; -const style = { height: '100%', minWidth: '100px' }; - const LensEmbeddableComponent: React.FC = ({ applyGlobalQueriesAndFilters = true, extraActions, @@ -65,7 +63,16 @@ const LensEmbeddableComponent: React.FC = ({ stackByField, timerange, width: wrapperWidth, + withActions = true, }) => { + const style = useMemo( + () => ({ + height: wrapperHeight ?? '100%', + minWidth: '100px', + width: wrapperWidth ?? '100%', + }), + [wrapperHeight, wrapperWidth] + ); const { lens } = useKibana().services; const dispatch = useDispatch(); const [isShowingModal, setIsShowingModal] = useState(false); @@ -81,7 +88,6 @@ const LensEmbeddableComponent: React.FC = ({ stackByField, title: '', }); - const LensComponent = lens.EmbeddableComponent; const inspectActionProps = useMemo( () => ({ @@ -98,7 +104,7 @@ const LensEmbeddableComponent: React.FC = ({ extraActions, inspectActionProps, timeRange: timerange, - withActions: true, + withActions, }); const handleCloseModal = useCallback(() => { @@ -165,6 +171,10 @@ const LensEmbeddableComponent: React.FC = ({ [attributes?.state?.adHocDataViews] ); + if (!searchSessionId) { + return null; + } + if ( !attributes || (visualizationData?.responses != null && visualizationData?.responses?.length === 0) diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx index e0f6a5ab56d6..77fa5b02cede 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/mocks.tsx @@ -129,3 +129,50 @@ export const mockAttributes: LensAttributes = { }, ], }; + +export const mockExtraFilter = [ + { + meta: { + type: 'phrases', + key: '_index', + params: ['.alerts-security.alerts-default'], + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + match_phrase: { + _index: '.alerts-security.alerts-default', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, +]; + +export const mockRulePreviewFilter = (internalReferenceId: string, ruleId: string) => [ + { + meta: { + disabled: false, + negate: false, + alias: null, + index: internalReferenceId, + key: 'kibana.alert.rule.uuid', + field: 'kibana.alert.rule.uuid', + params: { + query: ruleId, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts index 5ef9b3eda38b..b761aba81210 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/types.ts @@ -57,6 +57,7 @@ export interface LensEmbeddableComponentProps { stackByField?: string; timerange: { from: string; to: string }; width?: string; + withActions?: boolean; } export enum RequestStatus { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts index 9c8ccdfc51cf..0de49b52d66f 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.test.ts @@ -66,11 +66,11 @@ describe(`useActions`, () => { expect(result.current[0].id).toEqual('inspect'); expect(result.current[0].order).toEqual(4); - expect(result.current[1].id).toEqual('openInLens'); + expect(result.current[1].id).toEqual('addToNewCase'); expect(result.current[1].order).toEqual(3); - expect(result.current[2].id).toEqual('addToNewCase'); + expect(result.current[2].id).toEqual('addToExistingCase'); expect(result.current[2].order).toEqual(2); - expect(result.current[3].id).toEqual('addToExistingCase'); + expect(result.current[3].id).toEqual('openInLens'); expect(result.current[3].order).toEqual(1); }); @@ -110,11 +110,11 @@ describe(`useActions`, () => { expect(result.current[0].id).toEqual('inspect'); expect(result.current[0].order).toEqual(4); - expect(result.current[1].id).toEqual('openInLens'); + expect(result.current[1].id).toEqual('addToNewCase'); expect(result.current[1].order).toEqual(3); - expect(result.current[2].id).toEqual('addToNewCase'); + expect(result.current[2].id).toEqual('addToExistingCase'); expect(result.current[2].order).toEqual(2); - expect(result.current[3].id).toEqual('addToExistingCase'); + expect(result.current[3].id).toEqual('openInLens'); expect(result.current[3].order).toEqual(1); expect(result.current[4].id).toEqual('mockExtraAction'); expect(result.current[4].order).toEqual(0); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts index eb8097ee77ad..504f30511caf 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_actions.ts @@ -34,9 +34,9 @@ export const useActions = ({ const { navigateToPrefilledEditor } = lens; const [defaultActions, setDefaultActions] = useState([ 'inspect', - 'openInLens', 'addToNewCase', 'addToExistingCase', + 'openInLens', ]); useEffect(() => { @@ -74,7 +74,7 @@ export const useActions = ({ const actions = useMemo( () => - defaultActions.reduce((acc, action) => { + defaultActions?.reduce((acc, action) => { if (action === 'inspect' && inspectActionProps != null) { return [ ...acc, @@ -141,7 +141,7 @@ const getOpenInLensAction = ({ callback }: { callback: () => void }): Action => async execute(context: ActionExecutionContext): Promise { callback(); }, - order: 3, + order: 1, }; }; @@ -168,7 +168,7 @@ const getAddToNewCaseAction = ({ callback(); }, disabled, - order: 2, + order: 3, }; }; @@ -222,6 +222,6 @@ const getAddToExistingCaseAction = ({ callback(); }, disabled, - order: 1, + order: 2, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx index 7451209120e0..6fd82d4b0e1e 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx @@ -205,6 +205,45 @@ describe('useLensAttributes', () => { expect(result?.current).toBeNull(); }); + it('should return null if stackByField is an empty string', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + dataViewId: 'security-solution-default', + indicesExist: false, + selectedPatterns: ['auditbeat-*'], + }); + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getExternalAlertLensAttributes, + stackByField: '', + }), + { wrapper } + ); + + expect(result?.current).toBeNull(); + }); + + it('should return null if extraOptions.breakDownField is an empty string', () => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + dataViewId: 'security-solution-default', + indicesExist: false, + selectedPatterns: ['auditbeat-*'], + }); + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getExternalAlertLensAttributes, + stackByField: 'kibana.alert.rule.name', + extraOptions: { + breakdownField: '', + }, + }), + { wrapper } + ); + + expect(result?.current).toBeNull(); + }); + it('should return Lens attributes if adHocDataViews exist', () => { (useSourcererDataView as jest.Mock).mockReturnValue({ dataViewId: 'security-solution-default', diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx index 1976a743e5fa..9b5ef16dddd2 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx @@ -89,7 +89,13 @@ export const useLensAttributes = ({ const hasAdHocDataViews = Object.values(attrs?.state?.adHocDataViews ?? {}).length > 0; const lensAttrsWithInjectedData = useMemo(() => { - if (lensAttributes == null && (getLensAttributes == null || stackByField == null)) { + if ( + lensAttributes == null && + (getLensAttributes == null || + stackByField == null || + stackByField?.length === 0 || + (extraOptions?.breakdownField != null && extraOptions?.breakdownField.length === 0)) + ) { return null; } @@ -117,6 +123,7 @@ export const useLensAttributes = ({ applyGlobalQueriesAndFilters, attrs, dataViewId, + extraOptions?.breakdownField, filters, getLensAttributes, hasAdHocDataViews, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx index c53835c0f86b..ee77776e60a9 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import type { RenderResult } from '@testing-library/react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { VisualizationEmbeddable } from './visualization_embeddable'; import * as inputActions from '../../store/inputs/actions'; @@ -29,11 +29,12 @@ jest.mock('./lens_embeddable'); jest.mock('../page/use_refetch_by_session', () => ({ useRefetchByRestartingSession: jest.fn(), })); - +jest.useFakeTimers(); let res: RenderResult; const mockSearchSessionId = 'mockSearchSessionId'; const mockSearchSessionIdDefault = 'mockSearchSessionIdDefault'; const mockRefetchByRestartingSession = jest.fn(); +const mockRefetchByDeletingSession = jest.fn(); const mockSetQuery = jest.spyOn(inputActions, 'setQuery'); const mockDeleteQuery = jest.spyOn(inputActions, 'deleteOneQuery'); const state: State = { @@ -41,6 +42,7 @@ const state: State = { }; const { storage } = createSecuritySolutionStorageMock(); const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + describe('VisualizationEmbeddable', () => { describe('when isDonut = false', () => { beforeEach(() => { @@ -55,6 +57,7 @@ describe('VisualizationEmbeddable', () => { }, }, refetchByRestartingSession: mockRefetchByRestartingSession, + refetchByDeletingSession: mockRefetchByDeletingSession, }); res = render( @@ -71,15 +74,16 @@ describe('VisualizationEmbeddable', () => { expect(res.getByTestId('lens-embeddable')).toBeInTheDocument(); }); - it('should set query', () => { - expect(mockSetQuery).toHaveBeenCalledTimes(1); - expect(mockSetQuery).toHaveBeenCalledWith({ - inputId: InputsModelId.global, - id: 'testId', - searchSessionId: mockSearchSessionId, - refetch: mockRefetchByRestartingSession, - loading: false, - inspect: null, + it('should refetch by delete session when no data exists', async () => { + await waitFor(() => { + expect(mockSetQuery).toHaveBeenCalledWith({ + inputId: InputsModelId.global, + id: 'testId', + searchSessionId: mockSearchSessionId, + refetch: mockRefetchByDeletingSession, + loading: false, + inspect: null, + }); }); }); @@ -92,6 +96,73 @@ describe('VisualizationEmbeddable', () => { }); }); + describe('when data exists', () => { + const mockState = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + queries: [ + { + id: 'testId', + inspect: { + dsl: [], + response: [ + '{\n "took": 4,\n "timed_out": false,\n "_shards": {\n "total": 3,\n "successful": 3,\n "skipped": 2,\n "failed": 0\n },\n "hits": {\n "total": 21300,\n "max_score": null,\n "hits": []\n },\n "aggregations": {\n "0": {\n "buckets": {\n "Critical": {\n "doc_count": 0\n },\n "High": {\n "doc_count": 0\n },\n "Low": {\n "doc_count": 21300\n },\n "Medium": {\n "doc_count": 0\n }\n }\n }\n }\n}', + ], + }, + isInspected: false, + loading: false, + selectedInspectIndex: 0, + searchSessionId: undefined, + refetch: jest.fn(), + }, + ], + }, + }, + }; + const mockStore = createStore(mockState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + jest.clearAllMocks(); + (useRefetchByRestartingSession as jest.Mock).mockReturnValue({ + session: { + current: { + start: jest + .fn() + .mockReturnValueOnce(mockSearchSessionId) + .mockReturnValue(mockSearchSessionIdDefault), + }, + }, + refetchByRestartingSession: mockRefetchByRestartingSession, + refetchByDeletingSession: mockRefetchByDeletingSession, + }); + res = render( + + + + ); + }); + + it('should refetch by restart session', async () => { + await waitFor(() => { + expect(mockSetQuery).toHaveBeenCalledWith({ + inputId: InputsModelId.global, + id: 'testId', + searchSessionId: mockSearchSessionId, + refetch: mockRefetchByRestartingSession, + loading: false, + inspect: null, + }); + }); + }); + }); + describe('when isDonut = true', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx index 7ec7c9ee168a..9bf8d14d1533 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/visualization_embeddable.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { css } from 'styled-components'; import { ChartLabel } from '../../../overview/components/detection_response/alerts_by_status/chart_label'; @@ -18,6 +18,7 @@ import { InputsModelId } from '../../store/inputs/constants'; import { useRefetchByRestartingSession } from '../page/use_refetch_by_session'; import { LensEmbeddable } from './lens_embeddable'; import type { EmbeddableData, VisualizationEmbeddableProps } from './types'; +import { useSourcererDataView } from '../../containers/sourcerer'; const VisualizationEmbeddableComponent: React.FC = (props) => { const dispatch = useDispatch(); @@ -28,18 +29,22 @@ const VisualizationEmbeddableComponent: React.FC = label, donutTextWrapperClassName, onLoad, - ...lensPorps + ...lensProps } = props; - const { session, refetchByRestartingSession } = useRefetchByRestartingSession({ - inputId, - queryId: id, - }); + const { session, refetchByRestartingSession, refetchByDeletingSession } = + useRefetchByRestartingSession({ + inputId, + queryId: id, + }); + const { indicesExist } = useSourcererDataView(lensProps.scopeId); + + const memorizedTimerange = useRef(lensProps.timerange); const getGlobalQuery = inputsSelectors.globalQueryByIdSelector(); - const { inspect } = useDeepEqualSelector((state) => getGlobalQuery(state, id)); + const { inspect, searchSessionId } = useDeepEqualSelector((state) => getGlobalQuery(state, id)); const visualizationData = inspect?.response ? parseVisualizationData(inspect?.response) : null; - const dataExists = visualizationData != null && visualizationData[0]?.hits.total !== 0; + const dataExists = visualizationData != null && visualizationData[0]?.hits?.total !== 0; const donutTextWrapperStyles = dataExists ? css` top: 40%; @@ -70,17 +75,42 @@ const VisualizationEmbeddableComponent: React.FC = ); useEffect(() => { - dispatch( - inputsActions.setQuery({ - inputId, - id, - searchSessionId: session.current.start(), - refetch: refetchByRestartingSession, - loading: false, - inspect: null, - }) - ); - }, [dispatch, inputId, id, refetchByRestartingSession, session]); + // This handles timerange update when (alert) indices not found + if ( + (!indicesExist && memorizedTimerange.current?.from !== lensProps.timerange.from) || + memorizedTimerange.current?.to !== lensProps.timerange.to + ) { + memorizedTimerange.current = lensProps.timerange; + dispatch(inputsActions.deleteOneQuery({ inputId, id })); + } + }, [dispatch, id, indicesExist, inputId, lensProps.timerange]); + + useEffect(() => { + // This handles initial mount and refetch when (alert) indices not found + if (!searchSessionId) { + setTimeout(() => { + dispatch( + inputsActions.setQuery({ + inputId, + id, + searchSessionId: session.current.start(), + refetch: dataExists ? refetchByRestartingSession : refetchByDeletingSession, + loading: false, + inspect: null, + }) + ); + }, 200); + } + }, [ + dispatch, + inputId, + id, + session, + dataExists, + refetchByRestartingSession, + searchSessionId, + refetchByDeletingSession, + ]); useEffect(() => { return () => { @@ -88,22 +118,26 @@ const VisualizationEmbeddableComponent: React.FC = }; }, [dispatch, id, inputId]); + if ((!lensProps.getLensAttributes && !lensProps.lensAttributes) || !lensProps.timerange) { + return null; + } + if (isDonut) { return ( : null} + title={dataExists ? : null} donutTextWrapperClassName={donutTextWrapperClassName} donutTextWrapperStyles={donutTextWrapperStyles} > - + ); } - return ; + return ; }; export const VisualizationEmbeddable = React.memo(VisualizationEmbeddableComponent); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 85512855580c..f75385fdd495 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -249,7 +249,10 @@ export const useMatrixHistogramCombined = ( const [missingDataLoading, missingDataResponse] = useMatrixHistogram({ ...matrixHistogramQueryProps, includeMissingData: false, - skip: skipMissingData || matrixHistogramQueryProps.filterQuery === undefined, + skip: + skipMissingData || + matrixHistogramQueryProps.filterQuery === undefined || + matrixHistogramQueryProps.skip, }); const combinedLoading = useMemo( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx index 4f538b64b31e..6c2e3fc008cf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/alerts_count.tsx @@ -12,8 +12,8 @@ import styled from 'styled-components'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; import type { AlertsCountAggregation } from './types'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; import { getMaxRiskSubAggregations, getUpToMaxBuckets, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx new file mode 100644 index 000000000000..93b3f9264294 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/chart_content.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import type { VisualizationEmbeddableProps } from '../../../../common/components/visualization_actions/types'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; +import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; +import { AlertsCount } from './alerts_count'; +import type { AlertsCountAggregation } from './types'; + +type ChartContentProps = { + isChartEmbeddablesEnabled: boolean; +} & VisualizationEmbeddableProps & { + isLoadingAlerts: boolean; + alertsData: AlertSearchResponse | null; + stackByField0: string; + stackByField1: string | undefined; + }; + +const ChartContentComponent = ({ + alertsData, + extraActions, + extraOptions, + getLensAttributes, + height, + id, + inspectTitle, + isChartEmbeddablesEnabled, + isLoadingAlerts, + scopeId, + stackByField0, + stackByField1, + timerange, +}: ChartContentProps) => { + return isChartEmbeddablesEnabled ? ( + + ) : alertsData != null ? ( + + ) : null; +}; + +export const ChartContent = React.memo(ChartContentComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx index fc752b243f9b..cc2e5ca8c78d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx @@ -10,12 +10,15 @@ import { waitFor, act } from '@testing-library/react'; import { mount } from 'enzyme'; import { AlertsCountPanel } from '.'; + +import type { Status } from '../../../../../common/detection_engine/schemas/common'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1 } from '../common/config'; import { TestProviders } from '../../../../common/mock'; import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu'; import { TABLE } from '../../../pages/detection_engine/chart_panels/chart_select/translations'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; const from = '2022-07-28T08:20:18.966Z'; const to = '2022-07-28T08:20:18.966Z'; @@ -50,18 +53,30 @@ jest.mock('../../../containers/detection_engine/alerts/use_query', () => { }; }); -describe('AlertsCountPanel', () => { - const defaultProps = { - inspectTitle: TABLE, - signalIndexName: 'signalIndexName', - stackByField0: DEFAULT_STACK_BY_FIELD, - stackByField1: DEFAULT_STACK_BY_FIELD1, - setStackByField0: jest.fn(), - setStackByField1: jest.fn(), - }; - const mockSetToggle = jest.fn(); - const mockUseQueryToggle = useQueryToggle as jest.Mock; +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../common/hooks', () => ({ + useInspectButton: jest.fn(), + useStackByFields: jest.fn(), +})); + +const defaultProps = { + inspectTitle: TABLE, + setStackByField0: jest.fn(), + setStackByField1: jest.fn(), + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, + signalIndexName: 'signalIndexName', + stackByField0: DEFAULT_STACK_BY_FIELD, + stackByField1: DEFAULT_STACK_BY_FIELD1, + status: 'open' as Status, +}; +const mockUseQueryToggle = useQueryToggle as jest.Mock; +const mockSetToggle = jest.fn(); +describe('AlertsCountPanel', () => { beforeEach(() => { jest.clearAllMocks(); mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); @@ -197,3 +212,35 @@ describe('AlertsCountPanel', () => { }); }); }); + +describe('when isChartEmbeddablesEnabled = true', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + }); + + it('renders LensEmbeddable', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy(); + }); + }); + + it('should skip calling getAlertsRiskQuery', async () => { + await act(async () => { + mount( + + + + ); + expect(mockUseQueryAlerts.mock.calls[0][0].skip).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index d35960cd2777..c0b4d8bff6df 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -6,6 +6,7 @@ */ import type { EuiComboBox } from '@elastic/eui'; +import type { Action } from '@kbn/ui-actions-plugin/public'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import React, { memo, useMemo, useState, useEffect, useCallback } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -21,22 +22,27 @@ import { InspectButtonContainer } from '../../../../common/components/inspect'; import { getAlertsCountQuery } from './helpers'; import * as i18n from './translations'; -import { AlertsCount } from './alerts_count'; import type { AlertsCountAggregation } from './types'; import { KpiPanel } from '../common/components'; import { useInspectButton } from '../common/hooks'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { FieldSelection } from '../../../../common/components/field_selection'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getAlertsTableLensAttributes as getLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_table'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { ChartContent } from './chart_content'; export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; interface AlertsCountPanelProps { alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd'; chartOptionsContextMenu?: (queryId: string) => React.ReactNode; + extraActions?: Action[]; filters?: Filter[]; inspectTitle: string; panelHeight?: number; query?: Query; + runtimeMappings?: MappingRuntimeFields; setStackByField0: (stackBy: string) => void; setStackByField0ComboboxInputRef?: (inputRef: HTMLInputElement | null) => void; setStackByField1: (stackBy: string | undefined) => void; @@ -48,13 +54,14 @@ interface AlertsCountPanelProps { stackByField1ComboboxRef?: React.RefObject>; stackByWidth?: number; title?: React.ReactNode; - runtimeMappings?: MappingRuntimeFields; } +const CHART_HEIGHT = '180px'; export const AlertsCountPanel = memo( ({ alignHeader, chartOptionsContextMenu, + extraActions, filters, inspectTitle, panelHeight, @@ -100,14 +107,24 @@ export const AlertsCountPanel = memo( setQuerySkip(!toggleStatus); }, [toggleStatus]); const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); + (newToggleStatus: boolean) => { + setToggleStatus(newToggleStatus); // toggle on = skipQuery false - setQuerySkip(!status); + setQuerySkip(!newToggleStatus); }, [setQuerySkip, setToggleStatus] ); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from, to }), [from, to]); + + const extraVisualizationOptions = useMemo( + () => ({ + breakdownField: stackByField1, + filters, + }), + [filters, stackByField1] + ); const { loading: isLoadingAlerts, data: alertsData, @@ -125,7 +142,7 @@ export const AlertsCountPanel = memo( runtimeMappings, }), indexName: signalIndexName, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, queryName: ALERTS_QUERY_NAMES.COUNT, }); @@ -151,13 +168,13 @@ export const AlertsCountPanel = memo( ]); useInspectButton({ - setQuery, - response, - request, - refetch, - uniqueQueryId, deleteQuery, loading: isLoadingAlerts, + refetch, + request, + response, + setQuery, + uniqueQueryId, }); return ( @@ -181,7 +198,9 @@ export const AlertsCountPanel = memo( toggleQuery={toggleQuery} > ( uniqueQueryId={uniqueQueryId} /> - {toggleStatus && alertsData != null && ( - - )} + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index c47353aa04b0..a90b86dc4f4c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -21,6 +21,7 @@ import * as helpers from './helpers'; import { mockAlertSearchResponse } from './mock_data'; import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu'; import { AlertsHistogramPanel, LEGEND_WITH_COUNTS_WIDTH } from '.'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/containers/query_toggle'); @@ -95,11 +96,25 @@ jest.mock('../../../containers/detection_engine/alerts/use_query', () => { useQueryAlerts: (...props: unknown[]) => mockUseQueryAlerts(...props), }; }); +jest.mock('../../../../common/hooks/use_experimental_features'); +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); + +jest.mock('../../../../common/components/page/use_refetch_by_session'); +jest.mock('../common/hooks', () => { + const actual = jest.requireActual('../common/hooks'); + return { + ...actual, + useInspectButton: jest.fn(), + }; +}); describe('AlertsHistogramPanel', () => { const defaultProps = { - signalIndexName: 'signalIndexName', setQuery: jest.fn(), + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, + signalIndexName: 'signalIndexName', updateDateRange: jest.fn(), }; @@ -698,4 +713,36 @@ describe('AlertsHistogramPanel', () => { }); }); }); + + describe('when isChartEmbeddablesEnabled = true', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + + (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); + }); + + it('renders LensEmbeddable', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="lens-embeddable"]').exists()).toBeTruthy(); + }); + }); + + it('should skip calling getAlertsRiskQuery', async () => { + await act(async () => { + mount( + + + + ); + expect(mockUseQueryAlerts.mock.calls[0][0].skip).toBeTruthy(); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 96f5b938a136..c948eb26a1ba 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -6,6 +6,7 @@ */ import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { Action } from '@kbn/ui-actions-plugin/public'; import type { Position } from '@elastic/charts'; import type { EuiComboBox, EuiTitleSize } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; @@ -50,6 +51,10 @@ import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; import { useQueryToggle } from '../../../../common/containers/query_toggle'; import { GROUP_BY_TOP_LABEL } from '../common/translations'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getAlertsHistogramLensAttributes as getLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_histogram'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -68,6 +73,8 @@ const OptionsFlexItem = styled(EuiFlexItem)` export const LEGEND_WITH_COUNTS_WIDTH = 300; // px +const ChartHeight = '170px'; + interface AlertsHistogramPanelProps { alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd'; chartHeight?: number; @@ -75,31 +82,32 @@ interface AlertsHistogramPanelProps { combinedQueries?: string; comboboxRef?: React.RefObject>; defaultStackByOption?: string; + extraActions?: Action[]; filters?: Filter[]; headerChildren?: React.ReactNode; inspectTitle?: string; + legendPosition?: Position; onFieldSelected?: (field: string) => void; /** Override all defaults, and only display this field */ onlyField?: AlertsStackByField; paddingSize?: 's' | 'm' | 'l' | 'none'; panelHeight?: number; - titleSize?: EuiTitleSize; query?: Query; - legendPosition?: Position; + runtimeMappings?: MappingRuntimeFields; setComboboxInputRef?: (inputRef: HTMLInputElement | null) => void; - signalIndexName: string | null; showCountsInLegend?: boolean; showGroupByPlaceholder?: boolean; showLegend?: boolean; showLinkToAlerts?: boolean; - showTotalAlertsCount?: boolean; showStackBy?: boolean; + showTotalAlertsCount?: boolean; + signalIndexName: string | null; stackByLabel?: string; stackByWidth?: number; timelineId?: string; title?: React.ReactNode; + titleSize?: EuiTitleSize; updateDateRange: UpdateDateRange; - runtimeMappings?: MappingRuntimeFields; hideQueryToggle?: boolean; } @@ -113,30 +121,31 @@ export const AlertsHistogramPanel = memo( combinedQueries, comboboxRef, defaultStackByOption = DEFAULT_STACK_BY_FIELD, + extraActions, filters, headerChildren, inspectTitle, + legendPosition = 'right', onFieldSelected, onlyField, paddingSize = 'm', panelHeight = PANEL_HEIGHT, query, - legendPosition = 'right', + runtimeMappings, setComboboxInputRef, - signalIndexName, showCountsInLegend = false, showGroupByPlaceholder = false, showLegend = true, showLinkToAlerts = false, - showTotalAlertsCount = false, showStackBy = true, + showTotalAlertsCount = false, + signalIndexName, stackByLabel, stackByWidth, timelineId, title = i18n.HISTOGRAM_HEADER, - updateDateRange, titleSize = 'm', - runtimeMappings, + updateDateRange, hideQueryToggle = false, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(false); @@ -170,13 +179,17 @@ export const AlertsHistogramPanel = memo( setQuerySkip(!toggleStatus); }, [toggleStatus]); const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); + (newToggleStatus: boolean) => { + setToggleStatus(newToggleStatus); // toggle on = skipQuery false - setQuerySkip(!status); + setQuerySkip(!newToggleStatus); }, [setQuerySkip, setToggleStatus] ); + + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from, to }), [from, to]); + const { loading: isLoadingAlerts, data: alertsData, @@ -193,7 +206,7 @@ export const AlertsHistogramPanel = memo( runtimeMappings ), indexName: signalIndexName, - skip: querySkip, + skip: querySkip || isChartEmbeddablesEnabled, queryName: ALERTS_QUERY_NAMES.HISTOGRAM, }); @@ -260,13 +273,13 @@ export const AlertsHistogramPanel = memo( }, [isInitialLoading, isLoadingAlerts, setIsInitialLoading]); useInspectButton({ - setQuery, - response, - request, - refetch, - uniqueQueryId, deleteQuery, loading: isLoadingAlerts, + refetch, + request, + response, + setQuery, + uniqueQueryId, }); useEffect(() => { @@ -352,7 +365,7 @@ export const AlertsHistogramPanel = memo( titleSize={titleSize} toggleStatus={toggleStatus} toggleQuery={hideQueryToggle ? undefined : toggleQuery} - showInspectButton={chartOptionsContextMenu == null} + showInspectButton={isChartEmbeddablesEnabled ? false : chartOptionsContextMenu == null} subtitle={!isInitialLoading && showTotalAlertsCount && totalAlerts} isInspectDisabled={isInspectDisabled} hideSubtitle @@ -392,7 +405,7 @@ export const AlertsHistogramPanel = memo( )} {headerChildren != null && headerChildren} - {chartOptionsContextMenu != null && ( + {chartOptionsContextMenu != null && !isChartEmbeddablesEnabled && ( {chartOptionsContextMenu(uniqueQueryId)} @@ -403,7 +416,22 @@ export const AlertsHistogramPanel = memo( {toggleStatus ? ( - isInitialLoading ? ( + isChartEmbeddablesEnabled ? ( + + ) : isInitialLoading ? ( ) : ( void) | null; uniqueQueryId: string; loading: boolean; + searchSessionId?: string; } /** @@ -33,6 +36,7 @@ export const useInspectButton = ({ uniqueQueryId, deleteQuery, loading, + searchSessionId, }: UseInspectButtonParams) => { useEffect(() => { if (refetch != null && setQuery != null) { @@ -44,6 +48,7 @@ export const useInspectButton = ({ }, loading, refetch, + searchSessionId, }); } @@ -52,15 +57,24 @@ export const useInspectButton = ({ deleteQuery({ id: uniqueQueryId }); } }; - }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery]); + }, [setQuery, loading, response, request, refetch, uniqueQueryId, deleteQuery, searchSessionId]); }; +export function isDataViewFieldSubtypeNested(field: Partial) { + const subTypeNested = field?.subType as IFieldSubTypeNested; + return !!subTypeNested?.nested?.path; +} + +export function isKeyword(field: Partial) { + return field.esTypes && field.esTypes?.indexOf('keyword') >= 0; +} + export function getAggregatableFields(fields: { [fieldName: string]: Partial; }): EuiComboBoxOptionOption[] { const result = []; for (const [key, field] of Object.entries(fields)) { - if (field.aggregatable === true) { + if (field.aggregatable === true && isKeyword(field) && !isDataViewFieldSubtypeNested(field)) { result.push({ label: key, value: key }); } } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx index 66fa9023eb3d..ed24aa377146 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.tsx @@ -38,6 +38,9 @@ import { useGlobalFullScreen } from '../../../../common/containers/use_full_scre import type { TimeframePreviewOptions } from '../../../pages/detection_engine/rules/types'; import { useLicense } from '../../../../common/hooks/use_license'; import { useKibana } from '../../../../common/lib/kibana'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getRulePreviewLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/rule_preview'; +import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable'; const LoadingChart = styled(EuiLoadingChart)` display: block; @@ -53,6 +56,8 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` export const ID = 'previewHistogram'; +const CHART_HEIGHT = 150; + interface PreviewHistogramProps { previewId: string; addNoiseWarning: () => void; @@ -89,6 +94,17 @@ export const PreviewHistogram = ({ const isEqlRule = useMemo(() => ruleType === 'eql', [ruleType]); const isMlRule = useMemo(() => ruleType === 'machine_learning', [ruleType]); + const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); + const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]); + + const extraVisualizationOptions = useMemo( + () => ({ + ruleId: previewId, + spaceId, + }), + [previewId, spaceId] + ); + const [isLoading, { data, inspect, totalCount, refetch }] = usePreviewHistogram({ previewId, startDate, @@ -96,12 +112,14 @@ export const PreviewHistogram = ({ spaceId, indexPattern, ruleType, + skip: isChartEmbeddablesEnabled, }); const license = useLicense(); const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.detections); const { globalFullScreen } = useGlobalFullScreen(); const previousPreviewId = usePrevious(previewId); + const previewQueryId = `${ID}-${previewId}`; useEffect(() => { if (previousPreviewId !== previewId && totalCount > 0) { @@ -113,9 +131,23 @@ export const PreviewHistogram = ({ useEffect((): void => { if (!isLoading && !isInitializing) { - setQuery({ id: `${ID}-${previewId}`, inspect, loading: isLoading, refetch }); + setQuery({ + id: previewQueryId, + inspect, + loading: isLoading, + refetch, + }); } - }, [setQuery, inspect, isLoading, isInitializing, refetch, previewId]); + }, [ + setQuery, + inspect, + isLoading, + isInitializing, + refetch, + previewId, + isChartEmbeddablesEnabled, + previewQueryId, + ]); const barConfig = useMemo( (): ChartSeriesConfigs => getHistogramConfig(endDate, startDate, !isEqlRule), @@ -158,14 +190,28 @@ export const PreviewHistogram = ({ {isLoading ? ( + ) : isChartEmbeddablesEnabled ? ( + ) : ( { const { uiSettings } = useKibana().services; @@ -55,9 +57,9 @@ export const usePreviewHistogram = ({ stackByField, startDate, includeMissingData: false, - skip: error != null, + skip: skip || error != null, }; - }, [startDate, endDate, filterQuery, spaceId, error, stackByField]); + }, [endDate, filterQuery, spaceId, stackByField, startDate, skip, error]); return useMatrixHistogramCombined(matrixHistogramRequest); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx index 4cef9f95dcc6..f87b1b3fcbbf 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.test.tsx @@ -9,6 +9,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { useAlertsLocalStorage } from './alerts_local_storage'; +import type { Status } from '../../../../../common/detection_engine/schemas/common'; import { RESET_GROUP_BY_FIELDS } from '../../../../common/components/chart_settings_popover/configurations/default/translations'; import { CHART_SETTINGS_POPOVER_ARIA_LABEL } from '../../../../common/components/chart_settings_popover/translations'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; @@ -17,9 +18,16 @@ import { TestProviders } from '../../../../common/mock'; import { ChartPanels } from '.'; jest.mock('./alerts_local_storage'); - jest.mock('../../../../common/containers/sourcerer'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../../common/components/page/use_refetch_by_session', () => ({ + useRefetchByRestartingSession: jest.fn().mockReturnValue({ + searchSessionId: 'mockSearchSessionId', + refetchByRestartingSession: jest.fn(), + }), +})); + jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -104,6 +112,7 @@ const defaultProps = { }, }, ], + filterGroup: 'open' as Status, isLoadingIndexPattern: false, query: { query: '', @@ -111,6 +120,8 @@ const defaultProps = { }, runtimeMappings: {}, signalIndexName: '.alerts-security.alerts-default', + showBuildingBlockAlerts: false, + showOnlyThreatIndicatorAlerts: false, updateDateRangeCallback: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx index 6ed2353165f1..1ee5bbf7cfe9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/chart_panels/index.tsx @@ -6,6 +6,7 @@ */ import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public'; import type { Filter, Query } from '@kbn/es-query'; import { EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; @@ -28,6 +29,7 @@ import { } from '../../../components/alerts_kpis/common/config'; import { AlertsCountPanel } from '../../../components/alerts_kpis/alerts_count_panel'; import { GROUP_BY_LABEL } from '../../../components/alerts_kpis/common/translations'; +import { RESET_GROUP_BY_FIELDS } from '../../../../common/components/chart_settings_popover/configurations/default/translations'; const TABLE_PANEL_HEIGHT = 330; // px const TRENT_CHART_HEIGHT = 127; // px @@ -112,6 +114,35 @@ const ChartPanelsComponent: React.FC = ({ onResetStackByField1(); }, [onResetStackByField0, onResetStackByField1]); + const resetGroupByFieldAction = useMemo( + () => [ + { + id: 'resetGroupByField', + + getDisplayName(context: ActionExecutionContext): string { + return RESET_GROUP_BY_FIELDS; + }, + getIconType(context: ActionExecutionContext): string | undefined { + return 'editorRedo'; + }, + type: 'actionButton', + async isCompatible(context: ActionExecutionContext): Promise { + return true; + }, + async execute(context: ActionExecutionContext): Promise { + onReset(); + updateCommonStackBy0(DEFAULT_STACK_BY_FIELD); + + if (updateCommonStackBy1 != null) { + updateCommonStackBy1(DEFAULT_STACK_BY_FIELD1); + } + }, + order: 5, + }, + ], + [onReset, updateCommonStackBy0, updateCommonStackBy1] + ); + const chartOptionsContextMenu = useCallback( (queryId: string) => ( = ({ [alertViewSelection, setAlertViewSelection] ); const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled'); + return (
{alertViewSelection === 'trend' && ( @@ -151,21 +183,22 @@ const ChartPanelsComponent: React.FC = ({ chartOptionsContextMenu={chartOptionsContextMenu} comboboxRef={stackByField0ComboboxRef} defaultStackByOption={trendChartStackBy} + extraActions={resetGroupByFieldAction} filters={alertsHistogramDefaultFilters} inspectTitle={i18n.TREND} - setComboboxInputRef={setStackByField0ComboboxInputRef} onFieldSelected={updateCommonStackBy0} panelHeight={TREND_CHART_PANEL_HEIGHT} query={query} + runtimeMappings={runtimeMappings} + setComboboxInputRef={setStackByField0ComboboxInputRef} showCountsInLegend={true} - showGroupByPlaceholder={true} + showGroupByPlaceholder={false} showTotalAlertsCount={false} + signalIndexName={signalIndexName} stackByLabel={GROUP_BY_LABEL} title={title} titleSize={'s'} - signalIndexName={signalIndexName} updateDateRange={updateDateRangeCallback} - runtimeMappings={runtimeMappings} /> )} @@ -179,6 +212,7 @@ const ChartPanelsComponent: React.FC = ({ = ({ runtimeMappings={runtimeMappings} setStackByField0={updateCommonStackBy0} setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef} - stackByField0ComboboxRef={stackByField0ComboboxRef} setStackByField1={updateCommonStackBy1} setStackByField1ComboboxInputRef={setStackByField1ComboboxInputRef} - stackByField1ComboboxRef={stackByField1ComboboxRef} signalIndexName={signalIndexName} stackByField0={countTableStackBy0} + stackByField0ComboboxRef={stackByField0ComboboxRef} stackByField1={countTableStackBy1} + stackByField1ComboboxRef={stackByField1ComboboxRef} title={title} /> )} @@ -208,23 +242,23 @@ const ChartPanelsComponent: React.FC = ({ addFilter={addFilter} alignHeader="flexStart" chartOptionsContextMenu={chartOptionsContextMenu} + filters={alertsHistogramDefaultFilters} inspectTitle={i18n.TREEMAP} isPanelExpanded={isTreemapPanelExpanded} - filters={alertsHistogramDefaultFilters} query={query} + riskSubAggregationField="kibana.alert.risk_score" + runtimeMappings={runtimeMappings} setIsPanelExpanded={setIsTreemapPanelExpanded} setStackByField0={updateCommonStackBy0} setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef} - stackByField0ComboboxRef={stackByField0ComboboxRef} setStackByField1={updateCommonStackBy1} setStackByField1ComboboxInputRef={setStackByField1ComboboxInputRef} - stackByField1ComboboxRef={stackByField1ComboboxRef} signalIndexName={signalIndexName} stackByField0={riskChartStackBy0} + stackByField0ComboboxRef={stackByField0ComboboxRef} stackByField1={riskChartStackBy1} + stackByField1ComboboxRef={stackByField1ComboboxRef} title={title} - riskSubAggregationField="kibana.alert.risk_score" - runtimeMappings={runtimeMappings} /> )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a64ff6ecd3d4..f5a044a0b287 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -139,6 +139,9 @@ jest.mock('../../components/alerts_table/timeline_actions/use_bulk_add_to_case_a useBulkAddToCaseActions: jest.fn(() => []), })); +jest.mock('../../../common/components/visualization_actions/lens_embeddable'); +jest.mock('../../../common/components/page/use_refetch_by_session'); + describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx index ee2d86faca6c..7d860e5a9961 100644 --- a/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/pages/navigation/authentications_query_tab_body.test.tsx @@ -17,10 +17,8 @@ jest.mock('../../../containers/authentications'); jest.mock('../../../../common/containers/query_toggle'); jest.mock('../../../../common/lib/kibana'); -jest.mock('react-router-dom', () => { - const actual = jest.requireActual('react-router-dom'); - return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; -}); +jest.mock('../../../../common/components/visualization_actions'); +jest.mock('../../../../common/components/visualization_actions/lens_embeddable'); describe('Authentications query tab body', () => { const mockUseAuthentications = useAuthentications as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx index 713ea0876ade..7a0f918d4e16 100644 --- a/x-pack/plugins/security_solution/public/helpers.tsx +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -20,13 +20,10 @@ import { CASES_FEATURE_ID, CASES_PATH, EXCEPTIONS_PATH, - HOSTS_PATH, LANDING_PATH, - NETWORK_PATH, RULES_PATH, SERVER_APP_ID, THREAT_INTELLIGENCE_PATH, - USERS_PATH, } from '../common/constants'; import type { FactoryQueryTypes, @@ -195,13 +192,6 @@ export const isThreatIntelligencePath = (pathname: string): boolean => { }); }; -export const isExplorePage = (pathname: string): boolean => { - return !!matchPath(pathname, { - path: `(${HOSTS_PATH}|${USERS_PATH}|${NETWORK_PATH})`, - strict: false, - }); -}; - export const getSubPluginRoutesByCapabilities = ( subPlugins: StartedSubPlugins, capabilities: Capabilities diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx index cc960b4130ff..e3ab3b292aab 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx @@ -242,7 +242,6 @@ export const AlertsByStatus = ({ label={STATUS_OPEN} title={} totalCount={openCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} @@ -272,7 +271,6 @@ export const AlertsByStatus = ({ label={STATUS_ACKNOWLEDGED} title={} totalCount={acknowledgedCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} @@ -299,7 +297,6 @@ export const AlertsByStatus = ({ label={STATUS_CLOSED} title={} totalCount={closedCount} - isChartEmbeddablesEnabled={isChartEmbeddablesEnabled} /> )} From d1194d488a6626731e1ee9cc43e599e1c7f48e8f Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 2 Feb 2023 10:20:46 -0500 Subject: [PATCH 14/35] [Security Solution][Endpoint] CLI dev tool to run Elastic Agent/Endpoint against an Elastic stack (#149903) ## Summary New CLI tool to assist in development with connecting a host VM with Elastic Agent installed and running to an Elastic stack. Feature include: - Works against any elastic stack, including `localhost` (developer setup) - Can be run multiple times against the same stack (get multiple hosts communicating with the stack) - Will run a Fleet Server instance (via Docker) if one is not already running on the targeted stack - Creates a new VM using [`multipass` from Canonical](https://multipass.run) and installs and runs Elastic agent - All supported arguments are optional and have defaults assigned them (defaults target a `localhost` developer setup). See below for output of `--help`. - Can defined the specific version of the Elastic Agent to run. If none is defined, the version of the stack will be used. `SNAPSHOT` version are also supported. - Can define the policy ID to use for Enrollment. If none is defined, tool will create a new Agent Policy with Endpoint integration for use by the tool. Subsequent runs of this tool will also use this one agent policy if it is already present in the system. - Tools requires (dependencies) `docker` and `multipass` to be installed (both are checked to be present when the tool runs --- .../index_fleet_endpoint_policy.ts | 6 +- .../scripts/endpoint/common/fleet_services.ts | 176 +++++++- .../endpoint/common/localhost_services.ts | 29 ++ .../scripts/endpoint/common/stack_services.ts | 30 ++ .../endpoint_agent_runner/elastic_endpoint.ts | 250 ++++++++++++ .../endpoint_agent_runner/fleet_server.ts | 384 ++++++++++++++++++ .../endpoint/endpoint_agent_runner/index.ts | 69 ++++ .../endpoint_agent_runner/pre_check.ts | 49 +++ .../endpoint/endpoint_agent_runner/runtime.ts | 61 +++ .../endpoint/endpoint_agent_runner/setup.ts | 24 ++ .../endpoint/endpoint_agent_runner/types.ts | 18 + .../scripts/endpoint/run_endpoint_agent.js | 9 + 12 files changed, 1101 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/common/localhost_services.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/pre_check.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/runtime.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/types.ts create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/run_endpoint_agent.js diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts index c7ccc728525d..1c2326832402 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy.ts @@ -33,7 +33,8 @@ export interface IndexedFleetEndpointPolicyResponse { export const indexFleetEndpointPolicy = async ( kbnClient: KbnClient, policyName: string, - endpointPackageVersion: string = '8.0.0' + endpointPackageVersion: string = '8.0.0', + agentPolicyName?: string ): Promise => { const response: IndexedFleetEndpointPolicyResponse = { integrationPolicies: [], @@ -42,7 +43,8 @@ export const indexFleetEndpointPolicy = async ( // Create Agent Policy first const newAgentPolicyData: CreateAgentPolicyRequest['body'] = { - name: `Policy for ${policyName} (${Math.random().toString(36).substr(2, 5)})`, + name: + agentPolicyName || `Policy for ${policyName} (${Math.random().toString(36).substr(2, 5)})`, description: `Policy created with endpoint data generator (${policyName})`, namespace: 'default', }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index a8bc5fb04a8d..effa09db0c38 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -6,10 +6,27 @@ */ import type { Client, estypes } from '@elastic/elasticsearch'; -import { AGENTS_INDEX } from '@kbn/fleet-plugin/common'; -import type { AgentStatus } from '@kbn/fleet-plugin/common'; +import type { + Agent, + AgentStatus, + GetAgentPoliciesRequest, + GetAgentPoliciesResponse, + GetAgentsResponse, +} from '@kbn/fleet-plugin/common'; +import { AGENT_API_ROUTES, agentPolicyRouteService, AGENTS_INDEX } from '@kbn/fleet-plugin/common'; import { pick } from 'lodash'; import { ToolingLog } from '@kbn/tooling-log'; +import type { KbnClient } from '@kbn/test'; +import type { GetFleetServerHostsResponse } from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; +import { + enrollmentAPIKeyRouteService, + fleetServerHostsRoutesService, +} from '@kbn/fleet-plugin/common/services'; +import type { + EnrollmentAPIKey, + GetAgentsRequest, + GetEnrollmentAPIKeysResponse, +} from '@kbn/fleet-plugin/common/types'; import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator'; const fleetGenerator = new FleetAgentGenerator(); @@ -64,3 +81,158 @@ export const checkInFleetAgent = async ( }, }); }; + +/** + * Query Fleet Agents API + * + * @param kbnClient + * @param options + */ +export const fetchFleetAgents = async ( + kbnClient: KbnClient, + options: GetAgentsRequest['query'] +): Promise => { + return kbnClient + .request({ + method: 'GET', + path: AGENT_API_ROUTES.LIST_PATTERN, + query: options, + }) + .then((response) => response.data); +}; + +/** + * Will keep querying Fleet list of agents until the given `hostname` shows up as healthy + * + * @param kbnClient + * @param hostname + * @param timeoutMs + */ +export const waitForHostToEnroll = async ( + kbnClient: KbnClient, + hostname: string, + timeoutMs: number = 30000 +): Promise => { + const started = new Date(); + const hasTimedOut = (): boolean => { + const elapsedTime = Date.now() - started.getTime(); + return elapsedTime > timeoutMs; + }; + let found: Agent | undefined; + + while (!found && !hasTimedOut()) { + found = await fetchFleetAgents(kbnClient, { + perPage: 1, + kuery: `(local_metadata.host.hostname.keyword : "${hostname}") and (status:online)`, + showInactive: false, + }).then((response) => response.items[0]); + + if (!found) { + // sleep and check again + await new Promise((r) => setTimeout(r, 2000)); + } + } + + if (!found) { + throw new Error(`Timed out waiting for host [${hostname}] to show up in Fleet`); + } + + return found; +}; + +/** + * Returns the URL for the default Fleet Server connected to the stack + * @param kbnClient + */ +export const fetchFleetServerUrl = async (kbnClient: KbnClient): Promise => { + const fleetServerListResponse = await kbnClient + .request({ + method: 'GET', + path: fleetServerHostsRoutesService.getListPath(), + query: { + perPage: 100, + }, + }) + .then((response) => response.data); + + // TODO:PT need to also pull in the Proxies and use that instead if defiend for url + + let url: string | undefined; + + for (const fleetServer of fleetServerListResponse.items) { + if (!url || fleetServer.is_default) { + url = fleetServer.host_urls[0]; + + if (fleetServer.is_default) { + break; + } + } + } + + return url; +}; + +/** + * Retrieve the API enrollment key for a given FLeet Agent Policy + * @param kbnClient + * @param agentPolicyId + */ +export const fetchAgentPolicyEnrollmentKey = async ( + kbnClient: KbnClient, + agentPolicyId: string +): Promise => { + const apiKey: EnrollmentAPIKey | undefined = await kbnClient + .request({ + method: 'GET', + path: enrollmentAPIKeyRouteService.getListPath(), + query: { kuery: `policy_id: "${agentPolicyId}"` }, + }) + .then((response) => response.data.items[0]); + + if (!apiKey) { + return; + } + + return apiKey.api_key; +}; + +/** + * Retrieves a list of Fleet Agent policies + * @param kbnClient + * @param options + */ +export const fetchAgentPolicyList = async ( + kbnClient: KbnClient, + options: GetAgentPoliciesRequest['query'] = {} +) => { + return kbnClient + .request({ + method: 'GET', + path: agentPolicyRouteService.getListPath(), + query: options, + }) + .then((response) => response.data); +}; + +/** + * Returns the Agent Version that matches the current stack version. Will use `SNAPSHOT` if + * appropriate too. + * @param kbnClient + */ +export const getAgentVersionMatchingCurrentStack = async ( + kbnClient: KbnClient +): Promise => { + const kbnStatus = await kbnClient.status.get(); + let version = kbnStatus.version.number; + + // Add `-SNAPSHOT` if version indicates it was from a snapshot or the build hash starts + // with `xxxxxxxxx` (value that seems to be present when running kibana from source) + if ( + kbnStatus.version.build_snapshot || + kbnStatus.version.build_hash.startsWith('XXXXXXXXXXXXXXX') + ) { + version += '-SNAPSHOT'; + } + + return version; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/localhost_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/localhost_services.ts new file mode 100644 index 000000000000..3b985f76dcd8 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/localhost_services.ts @@ -0,0 +1,29 @@ +/* + * 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 execa from 'execa'; + +const POSSIBLE_LOCALHOST_VALUES: readonly string[] = [ + 'localhost', + '127.0.0.1', + '0.0.0.0', + '::1', + '0000:0000:0000:0000:0000:0000:0000:0000', +]; + +export const getLocalhostRealIp = async (): Promise => { + // TODO:PT find better way to get host machine public IP. Command below is not x-platform + + return execa.commandSync( + "ipconfig getifaddr `scutil --dns |awk -F'[()]' '$1~/if_index/ {print $2;exit;}'`", + { shell: true } + ).stdout; +}; + +export const isLocalhost = (hostname: string): boolean => { + return POSSIBLE_LOCALHOST_VALUES.includes(hostname.toLowerCase()); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts index 213f839421a7..424f451c3fdc 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts @@ -9,6 +9,7 @@ import { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; import { KbnClient } from '@kbn/test'; import type { StatusResponse } from '@kbn/core-status-common-internal'; +import { getLocalhostRealIp, isLocalhost } from './localhost_services'; import { createSecuritySuperuser } from './security_user_services'; export interface RuntimeServices { @@ -19,6 +20,19 @@ export interface RuntimeServices { username: string; password: string; }>; + localhostRealIp: string; + kibana: { + url: string; + hostname: string; + port: string; + isLocalhost: boolean; + }; + elastic: { + url: string; + hostname: string; + port: string; + isLocalhost: boolean; + }; } interface CreateRuntimeServicesOptions { @@ -58,14 +72,30 @@ export const createRuntimeServices = async ({ } } + const kbnURL = new URL(kibanaUrl); + const esURL = new URL(elasticsearchUrl); + return { kbnClient: createKbnClient({ log, url: kibanaUrl, username, password }), esClient: createEsClient({ log, url: elasticsearchUrl, username, password }), log, + localhostRealIp: await getLocalhostRealIp(), user: { username, password, }, + kibana: { + url: kibanaUrl, + hostname: kbnURL.hostname, + port: kbnURL.port, + isLocalhost: isLocalhost(kbnURL.hostname), + }, + elastic: { + url: elasticsearchUrl, + hostname: esURL.hostname, + port: esURL.port, + isLocalhost: isLocalhost(esURL.hostname), + }, }; }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts new file mode 100644 index 000000000000..7df5030a7009 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/elastic_endpoint.ts @@ -0,0 +1,250 @@ +/* + * 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 { userInfo } from 'os'; +import execa from 'execa'; +import nodeFetch from 'node-fetch'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; +import chalk from 'chalk'; +import { getEndpointPackageInfo } from '../../../common/endpoint/index_data'; +import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { + fetchAgentPolicyEnrollmentKey, + fetchAgentPolicyList, + fetchFleetServerUrl, + waitForHostToEnroll, +} from '../common/fleet_services'; +import { getRuntimeServices } from './runtime'; + +interface ElasticArtifactSearchResponse { + manifest: { + 'last-update-time': string; + 'seconds-since-last-update': number; + }; + packages: { + [packageFileName: string]: { + architecture: string; + os: string[]; + type: string; + asc_url: string; + sha_url: string; + url: string; + }; + }; +} + +export const enrollEndpointHost = async () => { + const { + log, + kbnClient, + options: { version, policy }, + } = getRuntimeServices(); + + log.info(`Creating VM and enrolling Elastic Agent`); + log.indent(4); + + try { + const uniqueId = Math.random().toString(32).substring(2).substring(0, 4); + const username = userInfo().username.toLowerCase(); + const policyId: string = policy || (await getOrCreateAgentPolicyId()); + + if (!policyId) { + throw new Error(`No valid policy id provide or unable to create it`); + } + + if (!version) { + throw new Error(`No 'version' specified`); + } + + const [fleetServerHostUrl, enrollmentToken] = await Promise.all([ + fetchFleetServerUrl(kbnClient), + fetchAgentPolicyEnrollmentKey(kbnClient, policyId), + ]); + + if (!fleetServerHostUrl) { + throw new Error(`Fleet setting does not have a Fleet Server host defined!`); + } + + if (!enrollmentToken) { + throw new Error(`No API enrollment key found for policy id [${policyId}]`); + } + + const vmName = `${username}-dev-${uniqueId}`; + + log.info(`Creating VM named: ${vmName}`); + + await execa.command(`multipass launch --name ${vmName}`); + + log.verbose(await execa('multipass', ['info', vmName])); + + const agentDownloadUrl = await getAgentDownloadUrl(version); + const agentDownloadedFile = agentDownloadUrl.substring(agentDownloadUrl.lastIndexOf('/') + 1); + const vmDirName = agentDownloadedFile.replace(/\.tar\.gz$/, ''); + + log.info(`Downloading and installing agent`); + log.verbose(`Agent download:\n ${agentDownloadUrl}`); + + await execa.command( + `multipass exec ${vmName} -- curl -L ${agentDownloadUrl} -o ${agentDownloadedFile}` + ); + await execa.command(`multipass exec ${vmName} -- tar -zxf ${agentDownloadedFile}`); + await execa.command(`multipass exec ${vmName} -- rm -f ${agentDownloadedFile}`); + + const agentEnrollArgs = [ + 'exec', + + vmName, + + '--working-directory', + `/home/ubuntu/${vmDirName}`, + + '--', + + 'sudo', + + './elastic-agent', + + 'enroll', + + '--insecure', + + '--force', + + '--url', + fleetServerHostUrl, + + '--enrollment-token', + enrollmentToken, + ]; + + log.info(`Enrolling elastic agent with Fleet`); + log.verbose(`Command: multipass ${agentEnrollArgs.join(' ')}`); + + await execa(`multipass`, agentEnrollArgs); + + const runAgentCommand = `multipass exec ${vmName} --working-directory /home/ubuntu/${vmDirName} -- sudo ./elastic-agent \&>/dev/null`; + + log.info(`Running elastic agent`); + log.verbose(`Command: ${runAgentCommand}`); + + // About `timeout` option below + // The `multipass exec` command seems to have some issues when a command pass to it redirects output, + // as is with the command that runs endpoint. See https://github.com/canonical/multipass/issues/667 + // To get around it, `timeout` is set to 5s, which should be enough time for the command to be executed + // in the VM. + await execa.command(runAgentCommand, { timeout: 5000 }).catch((error) => { + if (error.originalMessage !== 'Timed out') { + throw error; + } + }); + + log.info(`Waiting for Agent to check-in with Fleet`); + await waitForHostToEnroll(kbnClient, vmName); + + log.info(`VM created using Multipass. + VM Name: ${vmName} + Elastic Agent Version: ${version} + + Shell access: ${chalk.bold(`multipass shell ${vmName}`)} + Delete VM: ${chalk.bold(`multipass delete -p ${vmName}${await getVmCountNotice()}`)} +`); + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const getAgentDownloadUrl = async (version: string): Promise => { + const { log } = getRuntimeServices(); + // TODO:PT use arch and platform of VM to build download file name below (will be needed if tools ever supports different types of VMs) + const agentFile = `elastic-agent-${version}-linux-arm64.tar.gz`; + const artifactSearchUrl = `https://artifacts-api.elastic.co/v1/search/${version}/${agentFile}`; + + log.verbose(`Retrieving elastic agent download URL from:\n ${artifactSearchUrl}`); + + const searchResult: ElasticArtifactSearchResponse = await nodeFetch(artifactSearchUrl).then( + (response) => { + if (!response.ok) { + throw new Error( + `Failed to search elastic's artifact repository: ${response.statusText} (HTTP ${response.status})` + ); + } + + return response.json(); + } + ); + + log.verbose(searchResult); + + if (!searchResult.packages[agentFile]) { + throw new Error(`Unable to find an Agent download URL for version [${version}]`); + } + + return searchResult.packages[agentFile].url; +}; + +const getOrCreateAgentPolicyId = async (): Promise => { + const { kbnClient, log } = getRuntimeServices(); + const username = userInfo().username.toLowerCase(); + const endpointPolicyName = `${username} test integration`; + const agentPolicyName = `${username} test policy`; + + const existingPolicy = await fetchAgentPolicyList(kbnClient, { + kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.name: "${agentPolicyName}"`, + }); + + if (existingPolicy.items[0]) { + log.info(`Using existing Fleet test agent policy`); + log.verbose(existingPolicy.items[0]); + + return existingPolicy.items[0].id; + } + + // Create new policy + const endpointPackageVersion = (await getEndpointPackageInfo(kbnClient)).version; + const response = await indexFleetEndpointPolicy( + kbnClient, + endpointPolicyName, + endpointPackageVersion, + agentPolicyName + ); + + const agentPolicy = response.agentPolicies[0]; + + log.info(`New agent policy with Endpoint integration created: + Name: ${agentPolicy.name} + Id: ${agentPolicy.id}`); + + log.verbose(JSON.stringify(response, null, 2)); + + return agentPolicy.id ?? ''; +}; + +const getVmCountNotice = async (threshold: number = 1): Promise => { + const response = await execa.command(`multipass list --format=json`); + + const output: { list: Array<{ ipv4: string; name: string; release: string; state: string }> } = + JSON.parse(response.stdout); + + if (output.list.length > threshold) { + return ` + +----------------------------------------------------------------- +${chalk.red('NOTE:')} ${chalk.bold( + `You currently have ${output.list.length} VMs running.` + )} Remember to delete those + no longer being used. + View running VMs: ${chalk.bold('multipass list')} + ----------------------------------------------------------------- +`; + } + + return ''; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts new file mode 100644 index 000000000000..e8891f04aa6e --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts @@ -0,0 +1,384 @@ +/* + * 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 { + AgentPolicy, + CreateAgentPolicyResponse, + GetPackagePoliciesResponse, + Output, + PackagePolicy, +} from '@kbn/fleet-plugin/common'; +import { + AGENT_POLICY_API_ROUTES, + FLEET_SERVER_PACKAGE, + PACKAGE_POLICY_API_ROUTES, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, +} from '@kbn/fleet-plugin/common'; +import { APP_API_ROUTES } from '@kbn/fleet-plugin/common/constants'; +import type { + FleetServerHost, + GenerateServiceTokenResponse, + GetOneOutputResponse, + GetOutputsResponse, + PutOutputRequest, +} from '@kbn/fleet-plugin/common/types'; +import { + fleetServerHostsRoutesService, + outputRoutesService, +} from '@kbn/fleet-plugin/common/services'; +import execa from 'execa'; +import type { + PostFleetServerHostsRequest, + PostFleetServerHostsResponse, +} from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; +import chalk from 'chalk'; +import { isLocalhost } from '../common/localhost_services'; +import { + fetchFleetAgents, + fetchFleetServerUrl, + waitForHostToEnroll, +} from '../common/fleet_services'; +import { getRuntimeServices } from './runtime'; + +export const runFleetServerIfNeeded = async () => { + const { + log, + kibana: { isLocalhost: isKibanaOnLocalhost }, + } = getRuntimeServices(); + + log.info(`Setting up fleet server (if necessary)`); + log.indent(4); + + const fleetServerAlreadyEnrolled = await isFleetServerEnrolled(); + + if (fleetServerAlreadyEnrolled) { + log.info(`Fleet server is already enrolled with Fleet. Nothing to do.`); + log.indent(-4); + return; + } + + try { + const fleetServerAgentPolicyId = await getOrCreateFleetServerAgentPolicyId(); + const serviceToken = await generateFleetServiceToken(); + + if (isKibanaOnLocalhost) { + await configureFleetIfNeeded(); + } + + await startFleetServerWithDocker({ + policyId: fleetServerAgentPolicyId, + serviceToken, + }); + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const isFleetServerEnrolled = async () => { + const { kbnClient } = getRuntimeServices(); + const policyId = (await getFleetServerPackagePolicy())?.policy_id; + + if (!policyId) { + return false; + } + + const fleetAgentsResponse = await fetchFleetAgents(kbnClient, { + kuery: `(policy_id: "${policyId}" and active : true) and (status:online)`, + showInactive: false, + perPage: 1, + }); + + return Boolean(fleetAgentsResponse.total); +}; + +const getFleetServerPackagePolicy = async (): Promise => { + const { kbnClient } = getRuntimeServices(); + + return kbnClient + .request({ + method: 'GET', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + query: { + perPage: 1, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${FLEET_SERVER_PACKAGE}"`, + }, + }) + .then((response) => response.data.items[0]); +}; + +const getOrCreateFleetServerAgentPolicyId = async (): Promise => { + const { log, kbnClient } = getRuntimeServices(); + + const existingFleetServerIntegrationPolicy = await getFleetServerPackagePolicy(); + + if (existingFleetServerIntegrationPolicy) { + log.verbose( + `Found existing Fleet Server Policy: ${JSON.stringify( + existingFleetServerIntegrationPolicy, + null, + 2 + )}` + ); + log.info( + `Using existing Fleet Server agent policy id: ${existingFleetServerIntegrationPolicy.policy_id}` + ); + + return existingFleetServerIntegrationPolicy.policy_id; + } + + log.info(`Creating new Fleet Server policy`); + + const createdFleetServerPolicy: AgentPolicy = await kbnClient + .request({ + method: 'POST', + path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN, + body: { + name: `Fleet Server policy (${Math.random().toString(32).substring(2)})`, + description: `Created by CLI Tool via: ${__filename}`, + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + // This will ensure the Fleet Server integration policy + // is also created and added to the agent policy + has_fleet_server: true, + }, + }) + .then((response) => response.data.item); + + log.indent(4); + log.info( + `Agent Policy created: ${createdFleetServerPolicy.name} (${createdFleetServerPolicy.id})` + ); + log.verbose(createdFleetServerPolicy); + log.indent(-4); + + return createdFleetServerPolicy.id; +}; + +const generateFleetServiceToken = async (): Promise => { + const { kbnClient, log } = getRuntimeServices(); + + const serviceToken: string = await kbnClient + .request({ + method: 'POST', + path: APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN, + body: {}, + }) + .then((response) => response.data.value); + + log.info(`New service token created.`); + + return serviceToken; +}; + +const startFleetServerWithDocker = async ({ + policyId, + serviceToken, +}: { + policyId: string; + serviceToken: string; +}) => { + const { + log, + localhostRealIp, + elastic: { url: elasticUrl, isLocalhost: isElasticOnLocalhost }, + kbnClient, + options: { version }, + } = getRuntimeServices(); + + log.info(`Starting a new fleet server using Docker`); + log.indent(4); + + const esURL = new URL(elasticUrl); + const containerName = `dev-fleet-server.${esURL.hostname}`; + let esUrlWithRealIp: string = elasticUrl; + + if (isElasticOnLocalhost) { + esURL.hostname = localhostRealIp; + esUrlWithRealIp = esURL.toString(); + } + + try { + const dockerArgs = [ + 'run', + + '--restart', + 'no', + + '--add-host', + 'host.docker.internal:host-gateway', + + '--rm', + + '--detach', + + '--name', + containerName, + + // The container's hostname will appear in Fleet when the agent enrolls + '--hostname', + containerName, + + '--env', + 'FLEET_SERVER_ENABLE=1', + + '--env', + `FLEET_SERVER_ELASTICSEARCH_HOST=${esUrlWithRealIp}`, + + '--env', + `FLEET_SERVER_SERVICE_TOKEN=${serviceToken}`, + + '--env', + `FLEET_SERVER_POLICY=${policyId}`, + + '--publish', + '8220:8220', + + `docker.elastic.co/beats/elastic-agent:${version}`, + ]; + + await execa('docker', ['kill', containerName]) + .then(() => { + log.verbose( + `Killed an existing container with name [${containerName}]. New one will be started.` + ); + }) + .catch((error) => { + log.verbose(`Attempt to kill currently running fleet-server container (if any) with name [${containerName}] was unsuccessful: + ${error} +(This is ok if one was not running already)`); + }); + + log.verbose(`docker arguments:\n${dockerArgs.join(' ')}`); + + const containerId = (await execa('docker', dockerArgs)).stdout; + + const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName); + + log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`); + + await addFleetServerHostToFleetSettings(`https://${localhostRealIp}:8220`); + + log.info(`Done. Fleet Server is running and connected to Fleet. + Container Name: ${containerName} + Container Id: ${containerId} + + View running output: ${chalk.bold(`docker attach ---sig-proxy=false ${containerName}`)} + Shell access: ${chalk.bold(`docker exec -it ${containerName} /bin/bash`)} +`); + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const configureFleetIfNeeded = async () => { + const { log, kbnClient, localhostRealIp } = getRuntimeServices(); + + log.info('Checking if Fleet needs to be configured'); + log.indent(4); + + try { + // make sure that all ES hostnames are using localhost real IP + const fleetOutputs = await kbnClient + .request({ + method: 'GET', + path: outputRoutesService.getListPath(), + }) + .then((response) => response.data); + + for (const { id, ...output } of fleetOutputs.items) { + if (output.type === 'elasticsearch') { + if (output.hosts) { + let needsUpdating = false; + const updatedHosts: Output['hosts'] = []; + + for (const host of output.hosts) { + const hostURL = new URL(host); + + if (isLocalhost(hostURL.hostname)) { + needsUpdating = true; + hostURL.hostname = localhostRealIp; + updatedHosts.push(hostURL.toString()); + + log.verbose( + `Fleet Settings for Elasticsearch Output [Name: ${ + output.name + } (id: ${id})]: Host [${host}] updated to [${hostURL.toString()}]` + ); + } else { + updatedHosts.push(host); + } + } + + if (needsUpdating) { + const update: PutOutputRequest['body'] = { + ...(output as PutOutputRequest['body']), // cast needed to quite TS - looks like the types for Output in fleet differ a bit between create/update + hosts: updatedHosts, + }; + + log.info(`Updating Fleet Settings for Output [${output.name} (${id})]`); + + await kbnClient.request({ + method: 'PUT', + path: outputRoutesService.getUpdatePath(id), + body: update, + }); + } + } + } + } + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } + + log.indent(-4); +}; + +const addFleetServerHostToFleetSettings = async ( + fleetServerHostUrl: string +): Promise => { + const { kbnClient, log } = getRuntimeServices(); + + log.info(`Updating Fleet with new fleet server host: ${fleetServerHostUrl}`); + log.indent(4); + + try { + const exitingFleetServerHostUrl = await fetchFleetServerUrl(kbnClient); + + const newFleetHostEntry: PostFleetServerHostsRequest['body'] = { + name: `Dev fleet server running on localhost`, + host_urls: [fleetServerHostUrl], + is_default: !exitingFleetServerHostUrl, + }; + + const { item } = await kbnClient + .request({ + method: 'POST', + path: fleetServerHostsRoutesService.getCreatePath(), + body: newFleetHostEntry, + }) + .then((response) => response.data); + + log.verbose(item); + log.indent(-4); + + return item; + } catch (error) { + log.error(error); + log.indent(-4); + throw error; + } +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts new file mode 100644 index 000000000000..587a75679e46 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/index.ts @@ -0,0 +1,69 @@ +/* + * 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 { RunFn } from '@kbn/dev-cli-runner'; +import { run } from '@kbn/dev-cli-runner'; +import { setupAll } from './setup'; + +const runSetupAll: RunFn = async (cliContext) => { + const username = cliContext.flags.username as string; + const password = cliContext.flags.password as string; + const kibanaUrl = cliContext.flags.kibanaUrl as string; + const elasticUrl = cliContext.flags.elasticUrl as string; + const version = cliContext.flags.version as string; + const policy = cliContext.flags.policy as string; + const log = cliContext.log; + + await setupAll({ + elasticUrl, + kibanaUrl, + username, + password, + version, + policy, + log, + }); +}; + +export const cli = () => { + run( + runSetupAll, + + // Options + { + description: ` + Enrolls a new host running Elastic Agent with Fleet. It will (if necessary) first create a + Fleet Server instance using Docker, and then it will initialize a new Ubuntu VM using + 'multipass', install Elastic Agent and enroll it with Fleet. Can be used multiple times + against the same stack.`, + flags: { + string: ['kibana', 'elastic', 'username', 'password', 'version', 'policy'], + default: { + kibanaUrl: 'http://127.0.0.1:5601', + elasticUrl: 'http://127.0.0.1:9200', + username: 'elastic', + password: 'changeme', + version: '', + policy: '', + }, + help: ` + --version Optional. The version of the Agent to use for enrolling the new host. + Default: uses the same version as the stack (kibana). Version + can also be from 'SNAPSHOT'. + Examples: 8.6.0, 8.7.0-SNAPSHOT + --policy Optional. An Agent Policy ID to use when enrolling the new Host + running Elastic Agent. + --username Optional. User name to be used for auth against elasticsearch and + kibana (Default: elastic). + --password Optional. Password associated with the username (Default: changeme) + --kibanaUrl Optional. The url to Kibana (Default: http://127.0.0.1:5601) + --elasticUrl Optional. The url to Elasticsearch (Default: http://127.0.0.1:9200) + `, + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/pre_check.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/pre_check.ts new file mode 100644 index 000000000000..df0934b8b666 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/pre_check.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import execa from 'execa'; +import { getRuntimeServices } from './runtime'; + +export const checkDependencies = async () => { + const { log } = getRuntimeServices(); + + log.info(`Checking dependencies`); + + // TODO:PT validate that ES / KBN is reachable + + await Promise.all([checkDocker(), checkVmRunner()]); +}; + +const checkDocker = async () => { + const { log } = getRuntimeServices(); + + try { + const dockerVersion = await execa('docker', ['--version']); + + log.verbose(`Using docker: ${dockerVersion.stdout}`); + } catch (err) { + log.verbose(err); + throw new Error( + `Docker not found on local machine [${err.message}]. Install it from: https://www.docker.com\n\n` + ); + } +}; + +const checkVmRunner = async () => { + const { log } = getRuntimeServices(); + + try { + const version = await execa('multipass', ['--version']); + + log.verbose(`Using 'multipass': ${version.stdout}`); + } catch (err) { + log.verbose(err); + throw new Error( + `Mutipass not found on local machine [${err.message}]. Install it from: https://multipass.run\n\n` + ); + } +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/runtime.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/runtime.ts new file mode 100644 index 000000000000..989b99690c4e --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/runtime.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ToolingLog } from '@kbn/tooling-log'; +import { getAgentVersionMatchingCurrentStack } from '../common/fleet_services'; +import type { StartRuntimeServicesOptions } from './types'; +import type { RuntimeServices } from '../common/stack_services'; +import { createRuntimeServices } from '../common/stack_services'; + +interface EndpointRunnerRuntimeServices extends RuntimeServices { + options: Required< + Omit + >; +} + +// Internal singleton storing the services for the current run +let runtimeServices: undefined | EndpointRunnerRuntimeServices; + +export const startRuntimeServices = async ({ + log = new ToolingLog(), + elasticUrl, + kibanaUrl, + username, + password, + ...otherOptions +}: StartRuntimeServicesOptions) => { + const stackServices = await createRuntimeServices({ + kibanaUrl, + elasticsearchUrl: elasticUrl, + username, + password, + log, + }); + + runtimeServices = { + ...stackServices, + options: { + ...otherOptions, + + version: + otherOptions.version || + (await getAgentVersionMatchingCurrentStack(stackServices.kbnClient)), + }, + }; +}; + +export const stopRuntimeServices = async () => { + runtimeServices = undefined; +}; + +export const getRuntimeServices = () => { + if (!runtimeServices) { + throw new Error(`Runtime services have not be initialized yet!`); + } + + return runtimeServices; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts new file mode 100644 index 000000000000..18ef51a35bcc --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/setup.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { runFleetServerIfNeeded } from './fleet_server'; +import { startRuntimeServices, stopRuntimeServices } from './runtime'; +import { checkDependencies } from './pre_check'; +import { enrollEndpointHost } from './elastic_endpoint'; +import type { StartRuntimeServicesOptions } from './types'; + +export const setupAll = async (options: StartRuntimeServicesOptions) => { + await startRuntimeServices(options); + + await checkDependencies(); + + await runFleetServerIfNeeded(); + + await enrollEndpointHost(); + + await stopRuntimeServices(); +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/types.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/types.ts new file mode 100644 index 000000000000..2af229bc74a5 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ToolingLog } from '@kbn/tooling-log'; + +export interface StartRuntimeServicesOptions { + kibanaUrl: string; + elasticUrl: string; + username: string; + password: string; + version: string; + policy: string; + log?: ToolingLog; +} diff --git a/x-pack/plugins/security_solution/scripts/endpoint/run_endpoint_agent.js b/x-pack/plugins/security_solution/scripts/endpoint/run_endpoint_agent.js new file mode 100644 index 000000000000..1a79b1456b9c --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/run_endpoint_agent.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../src/setup_node_env'); +require('./endpoint_agent_runner').cli(); From 59488afa5d7fe30b683d2035d73c1a09f1394b9f Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 2 Feb 2023 07:21:43 -0800 Subject: [PATCH 15/35] [DOCS] Add specifications for deprecated alert APIs (#149655) --- .../rules/rule-apis-passthru.asciidoc | 1118 ++++++++++++++ .../alerting/docs/openapi/bundled.json | 1313 +++++++++++++++++ .../alerting/docs/openapi/bundled.yaml | 858 +++++++++++ .../schemas/alert_response_properties.yaml | 82 + .../alerting/docs/openapi/entrypoint.yaml | 41 +- .../paths/s@{spaceid}@api@alerts@_find.yaml | 120 ++ .../paths/s@{spaceid}@api@alerts@_health.yaml | 83 ++ ...@{spaceid}@api@alerts@alert@{alertid}.yaml | 290 ++++ ...}@api@alerts@alert@{alertid}@_disable.yaml | 30 + ...d}@api@alerts@alert@{alertid}@_enable.yaml | 30 + ...@api@alerts@alert@{alertid}@_mute_all.yaml | 30 + ...pi@alerts@alert@{alertid}@_unmute_all.yaml | 30 + ...lert_instance@{alertinstanceid}@_mute.yaml | 37 + ...rt_instance@{alertinstanceid}@_unmute.yaml | 37 + ...{spaceid}@api@alerts@list_alert_types.yaml | 111 ++ 15 files changed, 4189 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/alerting/docs/openapi/components/schemas/alert_response_properties.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_find.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_health.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml create mode 100644 x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@list_alert_types.yaml diff --git a/docs/api-generated/rules/rule-apis-passthru.asciidoc b/docs/api-generated/rules/rule-apis-passthru.asciidoc index d92712adeb10..0626e0e43a8b 100644 --- a/docs/api-generated/rules/rule-apis-passthru.asciidoc +++ b/docs/api-generated/rules/rule-apis-passthru.asciidoc @@ -26,6 +26,19 @@ Any modifications made to this file will be overwritten.
  • get /s/{spaceId}/api/alerting/_health
  • get /s/{spaceId}/api/alerting/rule/{ruleId}
  • get /s/{spaceId}/api/alerting/rule_types
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_disable
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_enable
  • +
  • get /s/{spaceId}/api/alerts/alerts/_find
  • +
  • get /s/{spaceId}/api/alerts/alert/{alertId}
  • +
  • get /s/{spaceId}/api/alerts/alerts/list_alert_types
  • +
  • get /s/{spaceId}/api/alerts/alerts/_health
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_mute_all
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute
  • +
  • post /s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all
  • +
  • put /s/{spaceId}/api/alerts/alert/{alertId}
  • +
  • delete /s/{spaceId}/api/alerts/alert/{alertId}
  • post /s/{spaceId}/api/alerting/rule/{ruleId}/alert/{alertId}/_mute
  • post /s/{spaceId}/api/alerting/rule/{ruleId}/_mute_all
  • post /s/{spaceId}/api/alerting/rule/{ruleId}/alert/{alertId}/_unmute
  • @@ -812,6 +825,896 @@ Any modifications made to this file will be overwritten. 401_response

    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Create an alert. (legacyCreateAlert)
    +
    Deprecated in 7.13.0. Use the create rule API instead.
    + +

    Path parameters

    +
    +
    alertId (required)
    + +
    Path Parameter — An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: +
      +
    • application/json
    • +
    + +

    Request body

    +
    +
    Legacy_create_alert_request_properties Legacy_create_alert_request_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "alertTypeId" : ".index-threshold",
    +  "throttle" : "throttle",
    +  "updatedBy" : "elastic",
    +  "executionStatus" : {
    +    "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +    "status" : "ok"
    +  },
    +  "params" : {
    +    "key" : ""
    +  },
    +  "enabled" : true,
    +  "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +  "tags" : [ "tags", "tags" ],
    +  "createdAt" : "2022-12-05T23:36:58.284Z",
    +  "schedule" : {
    +    "interval" : "interval"
    +  },
    +  "notifyWhen" : "onActionGroupChange",
    +  "createdBy" : "elastic",
    +  "muteAll" : false,
    +  "name" : "my alert",
    +  "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "actions" : [ "{}", "{}" ],
    +  "apiKeyOwner" : "elastic",
    +  "updatedAt" : "2022-12-05T23:36:58.284Z"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + alert_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_disable
    +
    Disables an alert. (legacyDisableAlert)
    +
    Deprecated in 7.13.0. Use the disable rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_enable
    +
    Enables an alert. (legacyEnableAlert)
    +
    Deprecated in 7.13.0. Use the enable rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alerts/_find
    +
    Retrieves a paginated set of alerts. (legacyFindAlerts)
    +
    Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert params are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + +

    Query parameters

    +
    +
    default_search_operator (optional)
    + +
    Query Parameter — The default operator to use for the simple_query_string. default: OR
    fields (optional)
    + +
    Query Parameter — The fields to return in the attributes key of the response. default: null
    filter (optional)
    + +
    Query Parameter — A KQL string that you filter with an attribute from your saved object. It should look like savedObjectType.attributes.title: "myTitle". However, if you used a direct attribute of a saved object, such as updatedAt, you must define your filter, for example, savedObjectType.updatedAt > 2018-12-22. default: null
    has_reference (optional)
    + +
    Query Parameter — Filters the rules that have a relation with the reference objects with a specific type and identifier. default: null
    page (optional)
    + +
    Query Parameter — The page number to return. default: 1
    per_page (optional)
    + +
    Query Parameter — The number of alerts to return per page. default: 20
    search (optional)
    + +
    Query Parameter — An Elasticsearch simple_query_string query that filters the alerts in the response. default: null
    search_fields (optional)
    + +
    Query Parameter — The fields to perform the simple_query_string parsed query against. default: null
    sort_field (optional)
    + +
    Query Parameter — Determines which field is used to sort the results. The field must exist in the attributes key of the response. default: null
    sort_order (optional)
    + +
    Query Parameter — Determines the sort order. default: desc
    +
    + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "total" : 1,
    +  "perPage" : 6,
    +  "data" : [ {
    +    "alertTypeId" : ".index-threshold",
    +    "throttle" : "throttle",
    +    "updatedBy" : "elastic",
    +    "executionStatus" : {
    +      "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +      "status" : "ok"
    +    },
    +    "params" : {
    +      "key" : ""
    +    },
    +    "enabled" : true,
    +    "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +    "tags" : [ "tags", "tags" ],
    +    "createdAt" : "2022-12-05T23:36:58.284Z",
    +    "schedule" : {
    +      "interval" : "interval"
    +    },
    +    "notifyWhen" : "onActionGroupChange",
    +    "createdBy" : "elastic",
    +    "muteAll" : false,
    +    "name" : "my alert",
    +    "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "actions" : [ "{}", "{}" ],
    +    "apiKeyOwner" : "elastic",
    +    "updatedAt" : "2022-12-05T23:36:58.284Z"
    +  }, {
    +    "alertTypeId" : ".index-threshold",
    +    "throttle" : "throttle",
    +    "updatedBy" : "elastic",
    +    "executionStatus" : {
    +      "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +      "status" : "ok"
    +    },
    +    "params" : {
    +      "key" : ""
    +    },
    +    "enabled" : true,
    +    "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +    "tags" : [ "tags", "tags" ],
    +    "createdAt" : "2022-12-05T23:36:58.284Z",
    +    "schedule" : {
    +      "interval" : "interval"
    +    },
    +    "notifyWhen" : "onActionGroupChange",
    +    "createdBy" : "elastic",
    +    "muteAll" : false,
    +    "name" : "my alert",
    +    "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +    "actions" : [ "{}", "{}" ],
    +    "apiKeyOwner" : "elastic",
    +    "updatedAt" : "2022-12-05T23:36:58.284Z"
    +  } ],
    +  "page" : 0
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + legacyFindAlerts_200_response +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Retrieves an alert by its identifier. (legacyGetAlert)
    +
    Deprecated in 7.13.0. Use the get rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "alertTypeId" : ".index-threshold",
    +  "throttle" : "throttle",
    +  "updatedBy" : "elastic",
    +  "executionStatus" : {
    +    "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +    "status" : "ok"
    +  },
    +  "params" : {
    +    "key" : ""
    +  },
    +  "enabled" : true,
    +  "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +  "tags" : [ "tags", "tags" ],
    +  "createdAt" : "2022-12-05T23:36:58.284Z",
    +  "schedule" : {
    +    "interval" : "interval"
    +  },
    +  "notifyWhen" : "onActionGroupChange",
    +  "createdBy" : "elastic",
    +  "muteAll" : false,
    +  "name" : "my alert",
    +  "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "actions" : [ "{}", "{}" ],
    +  "apiKeyOwner" : "elastic",
    +  "updatedAt" : "2022-12-05T23:36:58.284Z"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + alert_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alerts/list_alert_types
    +
    Retrieves a list of alert types. (legacyGetAlertTypes)
    +
    Deprecated in 7.13.0. Use the get rule types API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "defaultActionGroupId" : "defaultActionGroupId",
    +  "isExportable" : true,
    +  "actionVariables" : {
    +    "context" : [ {
    +      "name" : "name",
    +      "description" : "description"
    +    }, {
    +      "name" : "name",
    +      "description" : "description"
    +    } ],
    +    "state" : [ {
    +      "name" : "name",
    +      "description" : "description"
    +    }, {
    +      "name" : "name",
    +      "description" : "description"
    +    } ],
    +    "params" : [ {
    +      "name" : "name",
    +      "description" : "description"
    +    }, {
    +      "name" : "name",
    +      "description" : "description"
    +    } ]
    +  },
    +  "actionGroups" : [ {
    +    "name" : "name",
    +    "id" : "id"
    +  }, {
    +    "name" : "name",
    +    "id" : "id"
    +  } ],
    +  "name" : "name",
    +  "producer" : "producer",
    +  "authorizedConsumers" : "{}",
    +  "recoveryActionGroup" : {
    +    "name" : "name",
    +    "id" : "id"
    +  },
    +  "enabledInLicense" : true,
    +  "id" : "id",
    +  "minimumLicenseRequired" : "minimumLicenseRequired"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/alerts/alerts/_health
    +
    Retrieves the health status of the alerting framework. (legacyGetAlertingHealth)
    +
    Deprecated in 7.13.0. Use the get alerting framework health API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "hasPermanentEncryptionKey" : true,
    +  "alertingFrameworkHealth" : {
    +    "executionHealth" : {
    +      "status" : "ok",
    +      "timestamp" : "2023-01-13T01:28:00.28Z"
    +    },
    +    "decryptionHealth" : {
    +      "status" : "ok",
    +      "timestamp" : "2023-01-13T01:28:00.28Z"
    +    },
    +    "readHealth" : {
    +      "status" : "ok",
    +      "timestamp" : "2023-01-13T01:28:00.28Z"
    +    }
    +  },
    +  "isSufficientlySecure" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + legacyGetAlertingHealth_200_response +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute
    +
    Mutes an alert instance. (legacyMuteAlertInstance)
    +
    Deprecated in 7.13.0. Use the mute alert API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — An identifier for the alert. default: null
    alertInstanceId (required)
    + +
    Path Parameter — An identifier for the alert instance. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_mute_all
    +
    Mutes all alert instances. (legacyMuteAllAlertInstances)
    +
    Deprecated in 7.13.0. Use the mute all alerts API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute
    +
    Unmutes an alert instance. (legacyUnmuteAlertInstance)
    +
    Deprecated in 7.13.0. Use the unmute alert API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — An identifier for the alert. default: null
    alertInstanceId (required)
    + +
    Path Parameter — An identifier for the alert instance. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all
    +
    Unmutes all alert instances. (legacyUnmuteAllAlertInstances)
    +
    Deprecated in 7.13.0. Use the unmute all alerts API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    put /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Updates the attributes for an alert. (legacyUpdateAlert)
    +
    Deprecated in 7.13.0. Use the update rule API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: +
      +
    • application/json
    • +
    + +

    Request body

    +
    +
    Legacy_update_alert_request_properties Legacy_update_alert_request_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "alertTypeId" : ".index-threshold",
    +  "throttle" : "throttle",
    +  "updatedBy" : "elastic",
    +  "executionStatus" : {
    +    "lastExecutionDate" : "2022-12-06T00:13:43.89Z",
    +    "status" : "ok"
    +  },
    +  "params" : {
    +    "key" : ""
    +  },
    +  "enabled" : true,
    +  "mutedInstanceIds" : [ "mutedInstanceIds", "mutedInstanceIds" ],
    +  "tags" : [ "tags", "tags" ],
    +  "createdAt" : "2022-12-05T23:36:58.284Z",
    +  "schedule" : {
    +    "interval" : "interval"
    +  },
    +  "notifyWhen" : "onActionGroupChange",
    +  "createdBy" : "elastic",
    +  "muteAll" : false,
    +  "name" : "my alert",
    +  "scheduledTaskId" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "id" : "b530fed0-74f5-11ed-9801-35303b735aef",
    +  "actions" : [ "{}", "{}" ],
    +  "apiKeyOwner" : "elastic",
    +  "updatedAt" : "2022-12-05T23:36:58.284Z"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + alert_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    delete /s/{spaceId}/api/alerts/alert/{alertId}
    +
    Permanently removes an alert. (legaryDeleteAlert)
    +
    Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    alertId (required)
    + +
    Path Parameter — The identifier for the alert. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    Up @@ -1140,7 +2043,15 @@ Any modifications made to this file will be overwritten.
    1. 401_response - Unsuccessful rule API response
    2. 404_response -
    3. +
    4. Legacy_create_alert_request_properties - Legacy create alert request properties
    5. +
    6. Legacy_create_alert_request_properties_schedule -
    7. +
    8. Legacy_update_alert_request_properties - Legacy update alert request properties
    9. +
    10. Legacy_update_alert_request_properties_actions_inner -
    11. +
    12. Legacy_update_alert_request_properties_schedule -
    13. actions_inner -
    14. +
    15. alert_response_properties - Legacy alert response properties
    16. +
    17. alert_response_properties_executionStatus -
    18. +
    19. alert_response_properties_schedule -
    20. create_rule_request - Create rule request
    21. findRules_200_response -
    22. findRules_has_reference_parameter -
    23. @@ -1160,6 +2071,16 @@ Any modifications made to this file will be overwritten.
    24. getRuleTypes_200_response_inner_authorized_consumers -
    25. getRuleTypes_200_response_inner_authorized_consumers_alerts -
    26. getRuleTypes_200_response_inner_recovery_action_group -
    27. +
    28. legacyFindAlerts_200_response -
    29. +
    30. legacyGetAlertTypes_200_response_inner -
    31. +
    32. legacyGetAlertTypes_200_response_inner_actionVariables -
    33. +
    34. legacyGetAlertTypes_200_response_inner_actionVariables_context_inner -
    35. +
    36. legacyGetAlertTypes_200_response_inner_recoveryActionGroup -
    37. +
    38. legacyGetAlertingHealth_200_response -
    39. +
    40. legacyGetAlertingHealth_200_response_alertingFrameworkHealth -
    41. +
    42. legacyGetAlertingHealth_200_response_alertingFrameworkHealth_decryptionHealth -
    43. +
    44. legacyGetAlertingHealth_200_response_alertingFrameworkHealth_executionHealth -
    45. +
    46. legacyGetAlertingHealth_200_response_alertingFrameworkHealth_readHealth -
    47. notify_when -
    48. rule_response_properties - Rule response properties
    49. rule_response_properties_execution_status -
    50. @@ -1195,6 +2116,63 @@ Any modifications made to this file will be overwritten.
      404
    +
    +

    Legacy_create_alert_request_properties - Legacy create alert request properties Up

    +
    +
    +
    actions (optional)
    +
    alertTypeId
    String The ID of the alert type that you want to call when the alert is scheduled to run.
    +
    consumer
    String The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges.
    +
    enabled (optional)
    Boolean Indicates if you want to run the alert on an interval basis after it is created.
    +
    name
    String A name to reference and search.
    +
    notifyWhen
    String The condition for throttling the notification.
    +
    Enum:
    +
    onActionGroupChange
    onActiveAlert
    onThrottleInterval
    +
    params
    Object The parameters to pass to the alert type executor params value. This will also validate against the alert type params validator, if defined.
    +
    schedule
    +
    tags (optional)
    array[String] A list of keywords to reference and search.
    +
    throttle (optional)
    String How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of 10m or 1h will prevent it from sending 90 notifications during this period.
    +
    +
    +
    +

    Legacy_create_alert_request_properties_schedule - Up

    +
    The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.
    +
    +
    interval (optional)
    String The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.
    +
    +
    +
    +

    Legacy_update_alert_request_properties - Legacy update alert request properties Up

    +
    +
    +
    actions (optional)
    +
    name
    String A name to reference and search.
    +
    notifyWhen
    String The condition for throttling the notification.
    +
    Enum:
    +
    onActionGroupChange
    onActiveAlert
    onThrottleInterval
    +
    params
    Object The parameters to pass to the alert type executor params value. This will also validate against the alert type params validator, if defined.
    +
    schedule
    +
    tags (optional)
    array[String] A list of keywords to reference and search.
    +
    throttle (optional)
    String How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of 10m or 1h will prevent it from sending 90 notifications during this period.
    +
    +
    +
    +

    Legacy_update_alert_request_properties_actions_inner - Up

    +
    +
    +
    actionTypeId
    String The identifier for the action type.
    +
    group
    String Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to default.
    +
    id
    String The ID of the action saved object to execute.
    +
    params
    Object The map to the params that the action type will receive. params are handled as Mustache templates and passed a default set of context.
    +
    +
    +
    +

    Legacy_update_alert_request_properties_schedule - Up

    +
    The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.
    +
    +
    interval (optional)
    String The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.
    +
    +

    actions_inner - Up

    @@ -1204,6 +2182,46 @@ Any modifications made to this file will be overwritten.
    params (optional)
    map[String, oas_any_type_not_mapped] The parameters for the action, which are sent to the connector. The params are handled as Mustache templates and passed a default set of context.
    +
    +

    alert_response_properties - Legacy alert response properties Up

    +
    +
    +
    actions (optional)
    +
    alertTypeId (optional)
    +
    apiKeyOwner (optional)
    +
    createdAt (optional)
    Date The date and time that the alert was created. format: date-time
    +
    createdBy (optional)
    String The identifier for the user that created the alert.
    +
    enabled (optional)
    Boolean Indicates whether the alert is currently enabled.
    +
    executionStatus (optional)
    +
    id (optional)
    String The identifier for the alert.
    +
    muteAll (optional)
    +
    mutedInstanceIds (optional)
    +
    name (optional)
    String The name of the alert.
    +
    notifyWhen (optional)
    +
    params (optional)
    +
    schedule (optional)
    +
    scheduledTaskId (optional)
    +
    tags (optional)
    +
    throttle (optional)
    +
    updatedAt (optional)
    +
    updatedBy (optional)
    String The identifier for the user that updated this alert most recently.
    +
    +
    +
    +

    alert_response_properties_executionStatus - Up

    +
    +
    +
    lastExecutionDate (optional)
    Date format: date-time
    +
    status (optional)
    +
    +
    +
    +

    alert_response_properties_schedule - Up

    +
    +
    +
    interval (optional)
    +
    +

    create_rule_request - Create rule request Up

    The create rule API request body varies depending on the type of rule and actions.
    @@ -1396,6 +2414,106 @@ Any modifications made to this file will be overwritten.
    name (optional)
    +
    +

    legacyFindAlerts_200_response - Up

    +
    +
    +
    data (optional)
    +
    page (optional)
    +
    perPage (optional)
    +
    total (optional)
    +
    +
    +
    +

    legacyGetAlertTypes_200_response_inner - Up

    +
    +
    +
    actionGroups (optional)
    array[getRuleTypes_200_response_inner_action_groups_inner] An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid.
    +
    actionVariables (optional)
    +
    authorizedConsumers (optional)
    Object The list of the plugins IDs that have access to the alert type.
    +
    defaultActionGroupId (optional)
    String The default identifier for the alert type group.
    +
    enabledInLicense (optional)
    Boolean Indicates whether the rule type is enabled based on the subscription.
    +
    id (optional)
    String The unique identifier for the alert type.
    +
    isExportable (optional)
    Boolean Indicates whether the alert type is exportable in Saved Objects Management UI.
    +
    minimumLicenseRequired (optional)
    String The subscriptions required to use the alert type.
    +
    name (optional)
    String The descriptive name of the alert type.
    +
    producer (optional)
    String An identifier for the application that produces this alert type.
    +
    recoveryActionGroup (optional)
    +
    +
    +
    +

    legacyGetAlertTypes_200_response_inner_actionVariables - Up

    +
    A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors.
    + +
    + +
    +

    legacyGetAlertTypes_200_response_inner_recoveryActionGroup - Up

    +
    An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used.
    +
    +
    id (optional)
    +
    name (optional)
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response - Up

    +
    +
    +
    alertingFrameworkHealth (optional)
    +
    hasPermanentEncryptionKey (optional)
    Boolean If false, the encrypted saved object plugin does not have a permanent encryption key.
    +
    isSufficientlySecure (optional)
    Boolean If false, security is enabled but TLS is not.
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth - Up

    +
    Three substates identify the health of the alerting framework: decryptionHealth, executionHealth, and readHealth.
    + +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth_decryptionHealth - Up

    +
    The timestamp and status of the alert decryption.
    +
    +
    status (optional)
    +
    Enum:
    +
    error
    ok
    warn
    +
    timestamp (optional)
    Date format: date-time
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth_executionHealth - Up

    +
    The timestamp and status of the alert execution.
    +
    +
    status (optional)
    +
    Enum:
    +
    error
    ok
    warn
    +
    timestamp (optional)
    Date format: date-time
    +
    +
    +
    +

    legacyGetAlertingHealth_200_response_alertingFrameworkHealth_readHealth - Up

    +
    The timestamp and status of the alert reading events.
    +
    +
    status (optional)
    +
    Enum:
    +
    error
    ok
    warn
    +
    timestamp (optional)
    Date format: date-time
    +
    +

    notify_when - Up

    Indicates how often alerts generate actions. Valid values include: onActionGroupChange: Actions run when the alert status changes; onActiveAlert: Actions run when the alert becomes active and at each check interval while the rule conditions are met; onThrottleInterval: Actions run when the alert becomes active and at the interval specified in the throttle property while the rule conditions are met.
    diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.json b/x-pack/plugins/alerting/docs/openapi/bundled.json index 9d0395f18235..a245616abe46 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.json +++ b/x-pack/plugins/alerting/docs/openapi/bundled.json @@ -1243,6 +1243,1207 @@ "url": "https://localhost:5601" } ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}": { + "delete": { + "summary": "Permanently removes an alert.", + "operationId": "legaryDeleteAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it.\n", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "get": { + "summary": "Retrieves an alert by its identifier.", + "operationId": "legacyGetAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alert_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "post": { + "summary": "Create an alert.", + "operationId": "legacyCreateAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the create rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "in": "path", + "name": "alertId", + "description": "An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy create alert request properties", + "type": "object", + "required": [ + "alertTypeId", + "consumer", + "name", + "notifyWhen", + "params", + "schedule" + ], + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "actionTypeId", + "group", + "id", + "params" + ], + "properties": { + "actionTypeId": { + "type": "string", + "description": "The identifier for the action type." + }, + "group": { + "type": "string", + "description": "Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`.\n" + }, + "id": { + "type": "string", + "description": "The ID of the action saved object to execute." + }, + "params": { + "type": "object", + "description": "The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context.\n" + } + } + } + }, + "alertTypeId": { + "type": "string", + "description": "The ID of the alert type that you want to call when the alert is scheduled to run." + }, + "consumer": { + "type": "string", + "description": "The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges." + }, + "enabled": { + "type": "boolean", + "description": "Indicates if you want to run the alert on an interval basis after it is created." + }, + "name": { + "type": "string", + "description": "A name to reference and search." + }, + "notifyWhen": { + "type": "string", + "description": "The condition for throttling the notification.", + "enum": [ + "onActionGroupChange", + "onActiveAlert", + "onThrottleInterval" + ] + }, + "params": { + "type": "object", + "description": "The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined." + }, + "schedule": { + "type": "object", + "description": "The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.\n", + "properties": { + "interval": { + "type": "string", + "description": "The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.", + "example": "10s" + } + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of keywords to reference and search." + }, + "throttle": { + "type": "string", + "description": "How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period.\n" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alert_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "put": { + "summary": "Updates the attributes for an alert.", + "operationId": "legacyUpdateAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the update rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy update alert request properties", + "type": "object", + "required": [ + "name", + "notifyWhen", + "params", + "schedule" + ], + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "actionTypeId", + "group", + "id", + "params" + ], + "properties": { + "actionTypeId": { + "type": "string", + "description": "The identifier for the action type." + }, + "group": { + "type": "string", + "description": "Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`.\n" + }, + "id": { + "type": "string", + "description": "The ID of the action saved object to execute." + }, + "params": { + "type": "object", + "description": "The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context.\n" + } + } + } + }, + "name": { + "type": "string", + "description": "A name to reference and search." + }, + "notifyWhen": { + "type": "string", + "description": "The condition for throttling the notification.", + "enum": [ + "onActionGroupChange", + "onActiveAlert", + "onThrottleInterval" + ] + }, + "params": { + "type": "object", + "description": "The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined." + }, + "schedule": { + "type": "object", + "description": "The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule.\n", + "properties": { + "interval": { + "type": "string", + "description": "The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute.", + "example": "1d" + } + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of keywords to reference and search." + }, + "throttle": { + "type": "string", + "description": "How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period.\n" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/alert_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_disable": { + "post": { + "summary": "Disables an alert.", + "operationId": "legacyDisableAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the disable rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_enable": { + "post": { + "summary": "Enables an alert.", + "operationId": "legacyEnableAlert", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the enable rule API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_mute_all": { + "post": { + "summary": "Mutes all alert instances.", + "operationId": "legacyMuteAllAlertInstances", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the mute all alerts API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all": { + "post": { + "summary": "Unmutes all alert instances.", + "operationId": "legacyUnmuteAllAlertInstances", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the unmute all alerts API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "The identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alerts/_find": { + "get": { + "summary": "Retrieves a paginated set of alerts.", + "operationId": "legacyFindAlerts", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.\n", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "default_search_operator", + "in": "query", + "description": "The default operator to use for the `simple_query_string`.", + "schema": { + "type": "string", + "default": "OR" + }, + "example": "OR" + }, + { + "name": "fields", + "in": "query", + "description": "The fields to return in the `attributes` key of the response.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "filter", + "in": "query", + "description": "A KQL string that you filter with an attribute from your saved object. It should look like `savedObjectType.attributes.title: \"myTitle\"`. However, if you used a direct attribute of a saved object, such as `updatedAt`, you must define your filter, for example, `savedObjectType.updatedAt > 2018-12-22`.\n", + "schema": { + "type": "string" + } + }, + { + "name": "has_reference", + "in": "query", + "description": "Filters the rules that have a relation with the reference objects with a specific type and identifier.", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + { + "name": "page", + "in": "query", + "description": "The page number to return.", + "schema": { + "type": "integer", + "default": 1 + }, + "example": 1 + }, + { + "name": "per_page", + "in": "query", + "description": "The number of alerts to return per page.", + "schema": { + "type": "integer", + "default": 20 + }, + "example": 20 + }, + { + "name": "search", + "in": "query", + "description": "An Elasticsearch `simple_query_string` query that filters the alerts in the response.", + "schema": { + "type": "string" + } + }, + { + "name": "search_fields", + "in": "query", + "description": "The fields to perform the `simple_query_string` parsed query against.", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + { + "name": "sort_field", + "in": "query", + "description": "Determines which field is used to sort the results. The field must exist in the `attributes` key of the response.\n", + "schema": { + "type": "string" + } + }, + { + "name": "sort_order", + "in": "query", + "description": "Determines the sort order.", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "example": "asc" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/alert_response_properties" + } + }, + "page": { + "type": "integer" + }, + "perPage": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alerts/_health": { + "get": { + "summary": "Retrieves the health status of the alerting framework.", + "operationId": "legacyGetAlertingHealth", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get alerting framework health API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "alertingFrameworkHealth": { + "type": "object", + "description": "Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`.\n", + "properties": { + "decryptionHealth": { + "type": "object", + "description": "The timestamp and status of the alert decryption.", + "properties": { + "status": { + "type": "string", + "example": "ok", + "enum": [ + "error", + "ok", + "warn" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2023-01-13T01:28:00.280Z" + } + } + }, + "executionHealth": { + "type": "object", + "description": "The timestamp and status of the alert execution.", + "properties": { + "status": { + "type": "string", + "example": "ok", + "enum": [ + "error", + "ok", + "warn" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2023-01-13T01:28:00.280Z" + } + } + }, + "readHealth": { + "type": "object", + "description": "The timestamp and status of the alert reading events.", + "properties": { + "status": { + "type": "string", + "example": "ok", + "enum": [ + "error", + "ok", + "warn" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2023-01-13T01:28:00.280Z" + } + } + } + } + }, + "hasPermanentEncryptionKey": { + "type": "boolean", + "description": "If `false`, the encrypted saved object plugin does not have a permanent encryption key.", + "example": true + }, + "isSufficientlySecure": { + "type": "boolean", + "description": "If `false`, security is enabled but TLS is not.", + "example": true + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alerts/list_alert_types": { + "get": { + "summary": "Retrieves a list of alert types.", + "operationId": "legacyGetAlertTypes", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get rule types API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "actionGroups": { + "description": "An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid.\n", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + }, + "actionVariables": { + "description": "A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors.\n", + "type": "object", + "properties": { + "context": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "params": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + }, + "state": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "authorizedConsumers": { + "description": "The list of the plugins IDs that have access to the alert type.", + "type": "object" + }, + "defaultActionGroupId": { + "description": "The default identifier for the alert type group.", + "type": "string" + }, + "enabledInLicense": { + "description": "Indicates whether the rule type is enabled based on the subscription.", + "type": "boolean" + }, + "id": { + "description": "The unique identifier for the alert type.", + "type": "string" + }, + "isExportable": { + "description": "Indicates whether the alert type is exportable in Saved Objects Management UI.", + "type": "boolean" + }, + "minimumLicenseRequired": { + "description": "The subscriptions required to use the alert type.", + "type": "string" + }, + "name": { + "description": "The descriptive name of the alert type.", + "type": "string" + }, + "producer": { + "description": "An identifier for the application that produces this alert type.", + "type": "string" + }, + "recoveryActionGroup": { + "description": "An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used.\n", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute": { + "post": { + "summary": "Mutes an alert instance.", + "operationId": "legacyMuteAlertInstance", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the mute alert API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "An identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + }, + { + "in": "path", + "name": "alertInstanceId", + "description": "An identifier for the alert instance.", + "required": true, + "schema": { + "type": "string", + "example": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute": { + "post": { + "summary": "Unmutes an alert instance.", + "operationId": "legacyUnmuteAlertInstance", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the unmute alert API instead.", + "tags": [ + "alerting" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "in": "path", + "name": "alertId", + "description": "An identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "41893910-6bca-11eb-9e0d-85d233e3ee35" + } + }, + { + "in": "path", + "name": "alertInstanceId", + "description": "An identifier for the alert instance.", + "required": true, + "schema": { + "type": "string", + "example": "dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2" + } + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] } }, "components": { @@ -1674,6 +2875,118 @@ "$ref": "#/components/schemas/throttle" } } + }, + "alert_response_properties": { + "title": "Legacy alert response properties", + "type": "object", + "properties": { + "actions": { + "type": "array", + "items": { + "type": "object" + } + }, + "alertTypeId": { + "type": "string", + "example": ".index-threshold" + }, + "apiKeyOwner": { + "type": "string", + "nullable": true, + "example": "elastic" + }, + "createdAt": { + "type": "string", + "description": "The date and time that the alert was created.", + "format": "date-time", + "example": "2022-12-05T23:36:58.284Z" + }, + "createdBy": { + "type": "string", + "description": "The identifier for the user that created the alert.", + "example": "elastic" + }, + "enabled": { + "type": "boolean", + "description": "Indicates whether the alert is currently enabled.", + "example": true + }, + "executionStatus": { + "type": "object", + "properties": { + "lastExecutionDate": { + "type": "string", + "format": "date-time", + "example": "2022-12-06T00:13:43.890Z" + }, + "status": { + "type": "string", + "example": "ok" + } + } + }, + "id": { + "type": "string", + "description": "The identifier for the alert.", + "example": "b530fed0-74f5-11ed-9801-35303b735aef" + }, + "muteAll": { + "type": "boolean", + "example": false + }, + "mutedInstanceIds": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "name": { + "type": "string", + "description": "The name of the alert.", + "example": "my alert" + }, + "notifyWhen": { + "type": "string", + "example": "onActionGroupChange" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "schedule": { + "type": "object", + "properties": { + "interval": { + "type": "string" + } + } + }, + "scheduledTaskId": { + "type": "string", + "example": "b530fed0-74f5-11ed-9801-35303b735aef" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "throttle": { + "type": "string", + "nullable": true + }, + "updatedAt": { + "type": "string", + "example": "2022-12-05T23:36:58.284Z" + }, + "updatedBy": { + "type": "string", + "description": "The identifier for the user that updated this alert most recently.", + "nullable": true, + "example": "elastic" + } + } } }, "examples": { diff --git a/x-pack/plugins/alerting/docs/openapi/bundled.yaml b/x-pack/plugins/alerting/docs/openapi/bundled.yaml index 6515377574aa..ad2ea6a72f15 100644 --- a/x-pack/plugins/alerting/docs/openapi/bundled.yaml +++ b/x-pack/plugins/alerting/docs/openapi/bundled.yaml @@ -766,6 +766,781 @@ paths: - url: https://localhost:5601 servers: - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}: + delete: + summary: Permanently removes an alert. + operationId: legaryDeleteAlert + deprecated: true + description: | + Deprecated in 7.13.0. Use the delete rule API instead. WARNING: After you delete an alert, you cannot recover it. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + get: + summary: Retrieves an alert by its identifier. + operationId: legacyGetAlert + deprecated: true + description: Deprecated in 7.13.0. Use the get rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/alert_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + post: + summary: Create an alert. + operationId: legacyCreateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the create rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - in: path + name: alertId + description: An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create alert request properties + type: object + required: + - alertTypeId + - consumer + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: | + Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: | + The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. + alertTypeId: + type: string + description: The ID of the alert type that you want to call when the alert is scheduled to run. + consumer: + type: string + description: The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges. + enabled: + type: boolean + description: Indicates if you want to run the alert on an interval basis after it is created. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: | + The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: 10s + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: | + How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/alert_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + put: + summary: Updates the attributes for an alert. + operationId: legacyUpdateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the update rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update alert request properties + type: object + required: + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: | + Grouping actions is recommended for escalations for different types of alert instances. If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: | + The map to the `params` that the action type will receive. `params` are handled as Mustache templates and passed a default set of context. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: | + The schedule specifying when this alert should be run. A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: 1d + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: | + How often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/alert_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_disable: + post: + summary: Disables an alert. + operationId: legacyDisableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the disable rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_enable: + post: + summary: Enables an alert. + operationId: legacyEnableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the enable rule API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_mute_all: + post: + summary: Mutes all alert instances. + operationId: legacyMuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the mute all alerts API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all: + post: + summary: Unmutes all alert instances. + operationId: legacyUnmuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the unmute all alerts API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alerts/_find: + get: + summary: Retrieves a paginated set of alerts. + operationId: legacyFindAlerts + deprecated: true + description: | + Deprecated in 7.13.0. Use the find rules API instead. NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + - name: default_search_operator + in: query + description: The default operator to use for the `simple_query_string`. + schema: + type: string + default: OR + example: OR + - name: fields + in: query + description: The fields to return in the `attributes` key of the response. + schema: + type: array + items: + type: string + - name: filter + in: query + description: | + A KQL string that you filter with an attribute from your saved object. It should look like `savedObjectType.attributes.title: "myTitle"`. However, if you used a direct attribute of a saved object, such as `updatedAt`, you must define your filter, for example, `savedObjectType.updatedAt > 2018-12-22`. + schema: + type: string + - name: has_reference + in: query + description: Filters the rules that have a relation with the reference objects with a specific type and identifier. + schema: + type: object + properties: + id: + type: string + type: + type: string + - name: page + in: query + description: The page number to return. + schema: + type: integer + default: 1 + example: 1 + - name: per_page + in: query + description: The number of alerts to return per page. + schema: + type: integer + default: 20 + example: 20 + - name: search + in: query + description: An Elasticsearch `simple_query_string` query that filters the alerts in the response. + schema: + type: string + - name: search_fields + in: query + description: The fields to perform the `simple_query_string` parsed query against. + schema: + oneOf: + - type: string + - type: array + items: + type: string + - name: sort_field + in: query + description: | + Determines which field is used to sort the results. The field must exist in the `attributes` key of the response. + schema: + type: string + - name: sort_order + in: query + description: Determines the sort order. + schema: + type: string + enum: + - asc + - desc + default: desc + example: asc + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/alert_response_properties' + page: + type: integer + perPage: + type: integer + total: + type: integer + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alerts/_health: + get: + summary: Retrieves the health status of the alerting framework. + operationId: legacyGetAlertingHealth + deprecated: true + description: Deprecated in 7.13.0. Use the get alerting framework health API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + alertingFrameworkHealth: + type: object + description: | + Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`. + properties: + decryptionHealth: + type: object + description: The timestamp and status of the alert decryption. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: '2023-01-13T01:28:00.280Z' + executionHealth: + type: object + description: The timestamp and status of the alert execution. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: '2023-01-13T01:28:00.280Z' + readHealth: + type: object + description: The timestamp and status of the alert reading events. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: '2023-01-13T01:28:00.280Z' + hasPermanentEncryptionKey: + type: boolean + description: If `false`, the encrypted saved object plugin does not have a permanent encryption key. + example: true + isSufficientlySecure: + type: boolean + description: If `false`, security is enabled but TLS is not. + example: true + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alerts/list_alert_types: + get: + summary: Retrieves a list of alert types. + operationId: legacyGetAlertTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get rule types API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + type: object + properties: + actionGroups: + description: | + An explicit list of groups for which the alert type can schedule actions, each with the action group's unique ID and human readable name. Alert actions validation uses this configuration to ensure that groups are valid. + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + actionVariables: + description: | + A list of action variables that the alert type makes available via context and state in action parameter templates, and a short human readable description. The Alert UI will use this information to prompt users for these variables in action parameter editors. + type: object + properties: + context: + type: array + items: + type: object + properties: + name: + type: string + description: + type: string + params: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + state: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + authorizedConsumers: + description: The list of the plugins IDs that have access to the alert type. + type: object + defaultActionGroupId: + description: The default identifier for the alert type group. + type: string + enabledInLicense: + description: Indicates whether the rule type is enabled based on the subscription. + type: boolean + id: + description: The unique identifier for the alert type. + type: string + isExportable: + description: Indicates whether the alert type is exportable in Saved Objects Management UI. + type: boolean + minimumLicenseRequired: + description: The subscriptions required to use the alert type. + type: string + name: + description: The descriptive name of the alert type. + type: string + producer: + description: An identifier for the application that produces this alert type. + type: string + recoveryActionGroup: + description: | + An action group to use when an alert instance goes from an active state to an inactive one. If it is not specified, the default recovered action group is used. + type: object + properties: + id: + type: string + name: + type: string + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute: + post: + summary: Mutes an alert instance. + operationId: legacyMuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the mute alert API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute: + post: + summary: Unmutes an alert instance. + operationId: legacyUnmuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the unmute alert API instead. + tags: + - alerting + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 components: securitySchemes: basicAuth: @@ -1096,6 +1871,89 @@ components: $ref: '#/components/schemas/tags' throttle: $ref: '#/components/schemas/throttle' + alert_response_properties: + title: Legacy alert response properties + type: object + properties: + actions: + type: array + items: + type: object + alertTypeId: + type: string + example: .index-threshold + apiKeyOwner: + type: string + nullable: true + example: elastic + createdAt: + type: string + description: The date and time that the alert was created. + format: date-time + example: '2022-12-05T23:36:58.284Z' + createdBy: + type: string + description: The identifier for the user that created the alert. + example: elastic + enabled: + type: boolean + description: Indicates whether the alert is currently enabled. + example: true + executionStatus: + type: object + properties: + lastExecutionDate: + type: string + format: date-time + example: '2022-12-06T00:13:43.890Z' + status: + type: string + example: ok + id: + type: string + description: The identifier for the alert. + example: b530fed0-74f5-11ed-9801-35303b735aef + muteAll: + type: boolean + example: false + mutedInstanceIds: + type: array + nullable: true + items: + type: string + name: + type: string + description: The name of the alert. + example: my alert + notifyWhen: + type: string + example: onActionGroupChange + params: + type: object + additionalProperties: true + schedule: + type: object + properties: + interval: + type: string + scheduledTaskId: + type: string + example: b530fed0-74f5-11ed-9801-35303b735aef + tags: + type: array + items: + type: string + throttle: + type: string + nullable: true + updatedAt: + type: string + example: '2022-12-05T23:36:58.284Z' + updatedBy: + type: string + description: The identifier for the user that updated this alert most recently. + nullable: true + example: elastic examples: get_rule_response: summary: The get rule API returns a JSON object that contains details about the rule. diff --git a/x-pack/plugins/alerting/docs/openapi/components/schemas/alert_response_properties.yaml b/x-pack/plugins/alerting/docs/openapi/components/schemas/alert_response_properties.yaml new file mode 100644 index 000000000000..06fa627311e7 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/components/schemas/alert_response_properties.yaml @@ -0,0 +1,82 @@ +title: Legacy alert response properties +type: object +properties: + actions: + type: array + items: + type: object + alertTypeId: + type: string + example: ".index-threshold" + apiKeyOwner: + type: string + nullable: true + example: elastic + createdAt: + type: string + description: The date and time that the alert was created. + format: date-time + example: '2022-12-05T23:36:58.284Z' + createdBy: + type: string + description: The identifier for the user that created the alert. + example: elastic + enabled: + type: boolean + description: Indicates whether the alert is currently enabled. + example: true + executionStatus: + type: object + properties: + lastExecutionDate: + type: string + format: date-time + example: '2022-12-06T00:13:43.890Z' + status: + type: string + example: ok + id: + type: string + description: The identifier for the alert. + example: b530fed0-74f5-11ed-9801-35303b735aef + muteAll: + type: boolean + example: false + mutedInstanceIds: + type: array + nullable: true + items: + type: string + name: + type: string + description: The name of the alert. + example: my alert + notifyWhen: + type: string + example: onActionGroupChange + params: + type: object + additionalProperties: true + schedule: + type: object + properties: + interval: + type: string + scheduledTaskId: + type: string + example: b530fed0-74f5-11ed-9801-35303b735aef + tags: + type: array + items: + type: string + throttle: + type: string + nullable: true + updatedAt: + type: string + example: '2022-12-05T23:36:58.284Z' + updatedBy: + type: string + description: The identifier for the user that updated this alert most recently. + nullable: true + example: elastic \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml b/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml index 3b141954b30d..52b1babd68c8 100644 --- a/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/alerting/docs/openapi/entrypoint.yaml @@ -35,28 +35,27 @@ paths: $ref: 'paths/s@{spaceid}@api@alerting@rule@{ruleid}@alert@{alertid}@_mute.yaml' '/s/{spaceId}/api/alerting/rule/{ruleId}/alert/{alertId}/_unmute': $ref: 'paths/s@{spaceid}@api@alerting@rule@{ruleid}@alert@{alertid}@_unmute.yaml' - # Deprecated APIs -# '/s/{spaceId}/api/alerts/alert/{alertId}': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_disable': -# $ref: 'paths/s@{spaceid}@api@alertss@alert@{alertid}@_disable.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_enable': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_mute_all': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml' -# '/s/{spaceId}/api/alerts/alerts/_find': -# $ref: 'paths/s@{spaceid}@api@alerts@_find.yaml' -# '/s/{spaceId}/api/alerts/alerts/_health': -# $ref: 'paths/s@{spaceid}@api@alerts@_health.yaml' -# '/s/{spaceId}/api/alerts/alerts/list_alert_types': -# $ref: 'paths/s@{spaceid}@api@alerts@list_alert_types.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml' -# '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute': -# $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_disable': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_enable': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_mute_all': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/_unmute_all': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml' + '/s/{spaceId}/api/alerts/alerts/_find': + $ref: 'paths/s@{spaceid}@api@alerts@_find.yaml' + '/s/{spaceId}/api/alerts/alerts/_health': + $ref: 'paths/s@{spaceid}@api@alerts@_health.yaml' + '/s/{spaceId}/api/alerts/alerts/list_alert_types': + $ref: 'paths/s@{spaceid}@api@alerts@list_alert_types.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_mute': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml' + '/s/{spaceId}/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute': + $ref: 'paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml' components: securitySchemes: diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_find.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_find.yaml new file mode 100644 index 000000000000..bc8e2ecae490 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_find.yaml @@ -0,0 +1,120 @@ +get: + summary: Retrieves a paginated set of alerts. + operationId: legacyFindAlerts + deprecated: true + description: > + Deprecated in 7.13.0. Use the find rules API instead. + NOTE: Alert `params` are stored as a flattened field type and analyzed as keywords. + As alerts change in Kibana, the results on each page of the response also change. + Use the find API for traditional paginated results, but avoid using it to export large amounts of data. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + - name: default_search_operator + in: query + description: The default operator to use for the `simple_query_string`. + schema: + type: string + default: OR + example: OR + - name: fields + in: query + description: The fields to return in the `attributes` key of the response. + schema: + type: array + items: + type: string + - name: filter + in: query + description: > + A KQL string that you filter with an attribute from your saved object. + It should look like `savedObjectType.attributes.title: "myTitle"`. + However, if you used a direct attribute of a saved object, such as + `updatedAt`, you must define your filter, for example, + `savedObjectType.updatedAt > 2018-12-22`. + schema: + type: string + - name: has_reference + in: query + description: Filters the rules that have a relation with the reference objects with a specific type and identifier. + schema: + type: object + properties: + id: + type: string + type: + type: string + - name: page + in: query + description: The page number to return. + schema: + type: integer + default: 1 + example: 1 + - name: per_page + in: query + description: The number of alerts to return per page. + schema: + type: integer + default: 20 + example: 20 + - name: search + in: query + description: An Elasticsearch `simple_query_string` query that filters the alerts in the response. + schema: + type: string + - name: search_fields + in: query + description: The fields to perform the `simple_query_string` parsed query against. + schema: + oneOf: + - type: string + - type: array + items: + type: string + - name: sort_field + in: query + description: > + Determines which field is used to sort the results. The field must exist + in the `attributes` key of the response. + schema: + type: string + - name: sort_order + in: query + description: Determines the sort order. + schema: + type: string + enum: + - asc + - desc + default: desc + example: asc + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '../components/schemas/alert_response_properties.yaml' + page: + type: integer + perPage: + type: integer + total: + type: integer + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_health.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_health.yaml new file mode 100644 index 000000000000..2b9cd953596b --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@_health.yaml @@ -0,0 +1,83 @@ +get: + summary: Retrieves the health status of the alerting framework. + operationId: legacyGetAlertingHealth + deprecated: true + description: Deprecated in 7.13.0. Use the get alerting framework health API instead. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + alertingFrameworkHealth: + type: object + description: > + Three substates identify the health of the alerting framework: `decryptionHealth`, `executionHealth`, and `readHealth`. + properties: + decryptionHealth: + type: object + description: The timestamp and status of the alert decryption. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: "2023-01-13T01:28:00.280Z" + executionHealth: + type: object + description: The timestamp and status of the alert execution. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: "2023-01-13T01:28:00.280Z" + readHealth: + type: object + description: The timestamp and status of the alert reading events. + properties: + status: + type: string + example: ok + enum: + - error + - ok + - warn + timestamp: + type: string + format: date-time + example: "2023-01-13T01:28:00.280Z" + hasPermanentEncryptionKey: + type: boolean + description: If `false`, the encrypted saved object plugin does not have a permanent encryption key. + example: true + isSufficientlySecure: + type: boolean + description: If `false`, security is enabled but TLS is not. + example: true + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml new file mode 100644 index 000000000000..7976041b1448 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}.yaml @@ -0,0 +1,290 @@ +delete: + summary: Permanently removes an alert. + operationId: legaryDeleteAlert + deprecated: true + description: > + Deprecated in 7.13.0. Use the delete rule API instead. + WARNING: After you delete an alert, you cannot recover it. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +get: + summary: Retrieves an alert by its identifier. + operationId: legacyGetAlert + deprecated: true + description: Deprecated in 7.13.0. Use the get rule API instead. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/alert_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +post: + summary: Create an alert. + operationId: legacyCreateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the create rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - in: path + name: alertId + description: An UUID v1 or v4 identifier for the alert. If this parameter is omitted, the identifier is randomly generated. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create alert request properties + type: object + required: + - alertTypeId + - consumer + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: > + Grouping actions is recommended for escalations for different types of alert instances. + If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: > + The map to the `params` that the action type will receive. + `params` are handled as Mustache templates and passed a default set of context. + alertTypeId: + type: string + description: The ID of the alert type that you want to call when the alert is scheduled to run. + consumer: + type: string + description: The name of the application that owns the alert. This name has to match the Kibana feature name, as that dictates the required role-based access control privileges. + enabled: + type: boolean + description: Indicates if you want to run the alert on an interval basis after it is created. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: > + The schedule specifying when this alert should be run. + A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: "10s" + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: > + How often this alert should fire the same actions. + This will prevent the alert from sending out the same notification over and over. + For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, + setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/alert_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +put: + summary: Updates the attributes for an alert. + operationId: legacyUpdateAlert + deprecated: true + description: Deprecated in 7.13.0. Use the update rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update alert request properties + type: object + required: + - name + - notifyWhen + - params + - schedule + properties: + actions: + type: array + items: + type: object + required: + - actionTypeId + - group + - id + - params + properties: + actionTypeId: + type: string + description: The identifier for the action type. + group: + type: string + description: > + Grouping actions is recommended for escalations for different types of alert instances. + If you don't need this functionality, set it to `default`. + id: + type: string + description: The ID of the action saved object to execute. + params: + type: object + description: > + The map to the `params` that the action type will receive. + `params` are handled as Mustache templates and passed a default set of context. + name: + type: string + description: A name to reference and search. + notifyWhen: + type: string + description: The condition for throttling the notification. + enum: + - onActionGroupChange + - onActiveAlert + - onThrottleInterval + params: + type: object + description: The parameters to pass to the alert type executor `params` value. This will also validate against the alert type params validator, if defined. + schedule: + type: object + description: > + The schedule specifying when this alert should be run. + A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + properties: + interval: + type: string + description: The interval format specifies the interval in seconds, minutes, hours or days at which the alert should execute. + example: "1d" + tags: + type: array + items: + type: string + description: A list of keywords to reference and search. + throttle: + type: string + description: > + How often this alert should fire the same actions. + This will prevent the alert from sending out the same notification over and over. + For example, if an alert with a schedule of 1 minute stays in a triggered state for 90 minutes, + setting a throttle of `10m` or `1h` will prevent it from sending 90 notifications during this period. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/alert_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml new file mode 100644 index 000000000000..09e27e410db0 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_disable.yaml @@ -0,0 +1,30 @@ +post: + summary: Disables an alert. + operationId: legacyDisableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the disable rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml new file mode 100644 index 000000000000..8a8af9f7a748 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_enable.yaml @@ -0,0 +1,30 @@ +post: + summary: Enables an alert. + operationId: legacyEnableAlert + deprecated: true + description: Deprecated in 7.13.0. Use the enable rule API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml new file mode 100644 index 000000000000..48c55a553e19 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_mute_all.yaml @@ -0,0 +1,30 @@ +post: + summary: Mutes all alert instances. + operationId: legacyMuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the mute all alerts API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml new file mode 100644 index 000000000000..8749d657b2de --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@_unmute_all.yaml @@ -0,0 +1,30 @@ +post: + summary: Unmutes all alert instances. + operationId: legacyUnmuteAllAlertInstances + deprecated: true + description: Deprecated in 7.13.0. Use the unmute all alerts API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: The identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml new file mode 100644 index 000000000000..ca407b420ece --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_mute.yaml @@ -0,0 +1,37 @@ +post: + summary: Mutes an alert instance. + operationId: legacyMuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the mute alert API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml new file mode 100644 index 000000000000..85d6ef9c4384 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@alert@{alertid}@alert_instance@{alertinstanceid}@_unmute.yaml @@ -0,0 +1,37 @@ +post: + summary: Unmutes an alert instance. + operationId: legacyUnmuteAlertInstance + deprecated: true + description: Deprecated in 7.13.0. Use the unmute alert API instead. + tags: + - alerting + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 41893910-6bca-11eb-9e0d-85d233e3ee35 + - in: path + name: alertInstanceId + description: An identifier for the alert instance. + required: true + schema: + type: string + example: dceeb5d0-6b41-11eb-802b-85b0c1bc8ba2 + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@list_alert_types.yaml b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@list_alert_types.yaml new file mode 100644 index 000000000000..1f23d5c94e29 --- /dev/null +++ b/x-pack/plugins/alerting/docs/openapi/paths/s@{spaceid}@api@alerts@list_alert_types.yaml @@ -0,0 +1,111 @@ +get: + summary: Retrieves a list of alert types. + operationId: legacyGetAlertTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get rule types API instead. + tags: + - alerting + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + type: object + properties: + actionGroups: + description: > + An explicit list of groups for which the alert type can + schedule actions, each with the action group's unique ID and + human readable name. Alert actions validation uses this + configuration to ensure that groups are valid. + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + actionVariables: + description: > + A list of action variables that the alert type makes available + via context and state in action parameter templates, and a + short human readable description. The Alert UI will use this + information to prompt users for these variables in action + parameter editors. + type: object + properties: + context: + type: array + items: + type: object + properties: + name: + type: string + description: + type: string + params: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + state: + type: array + items: + type: object + properties: + description: + type: string + name: + type: string + authorizedConsumers: + description: The list of the plugins IDs that have access to the alert type. + type: object + defaultActionGroupId: + description: The default identifier for the alert type group. + type: string + enabledInLicense: + description: Indicates whether the rule type is enabled based on the subscription. + type: boolean + id: + description: The unique identifier for the alert type. + type: string + isExportable: + description: Indicates whether the alert type is exportable in Saved Objects Management UI. + type: boolean + minimumLicenseRequired: + description: The subscriptions required to use the alert type. + type: string + name: + description: The descriptive name of the alert type. + type: string + producer: + description: An identifier for the application that produces this alert type. + type: string + recoveryActionGroup: + description: > + An action group to use when an alert instance goes from an active state to an inactive one. + If it is not specified, the default recovered action group is used. + type: object + properties: + id: + type: string + name: + type: string + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' +servers: + - url: https://localhost:5601 \ No newline at end of file From 83c382b0887754376840131a2b8e8208d93f855d Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 2 Feb 2023 07:22:32 -0800 Subject: [PATCH 16/35] [DOCS] Add specifications for deprecated connector APIs (#149637) --- .../connector-apis-passthru.asciidoc | 520 +++++++++++++++++- .../plugins/actions/docs/openapi/bundled.json | 514 ++++++++++++++++- .../plugins/actions/docs/openapi/bundled.yaml | 314 ++++++++++- .../openapi/components/headers/kbn_xsrf.yaml | 1 + .../components/parameters/action_id.yaml | 7 + .../schemas/action_response_properties.yaml | 21 + .../schemas/config_properties_servicenow.yaml | 2 +- .../config_properties_servicenow_itom.yaml | 2 +- .../actions/docs/openapi/entrypoint.yaml | 18 +- .../paths/s@{spaceid}@api@actions.yaml | 78 +++ ...paceid}@api@actions@action@{actionid}.yaml | 98 ++++ ...pi@actions@action@{actionid}@_execute.yaml | 57 ++ ...paceid}@api@actions@list_action_types.yaml | 50 ++ 13 files changed, 1660 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/actions/docs/openapi/components/parameters/action_id.yaml create mode 100644 x-pack/plugins/actions/docs/openapi/components/schemas/action_response_properties.yaml create mode 100644 x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions.yaml create mode 100644 x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}.yaml create mode 100644 x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml create mode 100644 x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@list_action_types.yaml diff --git a/docs/api-generated/connectors/connector-apis-passthru.asciidoc b/docs/api-generated/connectors/connector-apis-passthru.asciidoc index 98ef93db6e82..c51d4332bc50 100644 --- a/docs/api-generated/connectors/connector-apis-passthru.asciidoc +++ b/docs/api-generated/connectors/connector-apis-passthru.asciidoc @@ -23,6 +23,13 @@ Any modifications made to this file will be overwritten.
  • get /s/{spaceId}/api/actions/connector/{connectorId}
  • get /s/{spaceId}/api/actions/connector_types
  • get /s/{spaceId}/api/actions/connectors
  • +
  • post /s/{spaceId}/api/actions
  • +
  • delete /s/{spaceId}/api/actions/action/{actionId}
  • +
  • get /s/{spaceId}/api/actions/action/{actionId}
  • +
  • get /s/{spaceId}/api/actions/list_action_types
  • +
  • get /s/{spaceId}/api/actions
  • +
  • post /s/{spaceId}/api/actions/action/{actionId}/_execute
  • +
  • put /s/{spaceId}/api/actions/action/{actionId}
  • post /s/{spaceId}/api/actions/connector/{connectorId}/_execute
  • put /s/{spaceId}/api/actions/connector/{connectorId}
  • @@ -60,7 +67,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -116,7 +123,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -316,6 +323,441 @@ Any modifications made to this file will be overwritten. 401_response

    +
    +
    + Up +
    post /s/{spaceId}/api/actions
    +
    Creates a connector. (legacyCreateConnector)
    +
    Deprecated in 7.13.0. Use the create connector API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: +
      +
    • application/json
    • +
    + +

    Request body

    +
    +
    Legacy_create_connector_request_properties Legacy_create_connector_request_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + action_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    delete /s/{spaceId}/api/actions/action/{actionId}
    +
    Deletes a connector. (legacyDeleteConnector)
    +
    Deprecated in 7.13.0. Use the delete connector API instead. WARNING: When you delete a connector, it cannot be recovered.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + + + + + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    204

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/actions/action/{actionId}
    +
    Retrieves a connector by ID. (legacyGetConnector)
    +
    Deprecated in 7.13.0. Use the get connector API instead.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + action_response_properties +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/actions/list_action_types
    +
    Retrieves a list of all connector types. (legacyGetConnectorTypes)
    +
    Deprecated in 7.13.0. Use the get all connector types API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "enabledInConfig" : true,
    +  "name" : "name",
    +  "enabledInLicense" : true,
    +  "id" : "id",
    +  "minimumLicenseRequired" : "minimumLicenseRequired",
    +  "enabled" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    get /s/{spaceId}/api/actions
    +
    Retrieves all connectors. (legacyGetConnectors)
    +
    Deprecated in 7.13.0. Use the get all connectors API instead.
    + +

    Path parameters

    +
    +
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + + + + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    post /s/{spaceId}/api/actions/action/{actionId}/_execute
    +
    Runs a connector. (legacyRunConnector)
    +
    Deprecated in 7.13.0. Use the run connector API instead.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: +
      +
    • application/json
    • +
    + +

    Request body

    +
    +
    Legacy_run_connector_request_body_properties Legacy_run_connector_request_body_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "actionId" : "actionId",
    +  "status" : "status"
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + legacyRunConnector_200_response +

    401

    + Authorization information is missing or invalid. + 401_response +
    +
    +
    +
    + Up +
    put /s/{spaceId}/api/actions/action/{actionId}
    +
    Updates the attributes for a connector. (legacyUpdateConnector)
    +
    Deprecated in 7.13.0. Use the update connector API instead.
    + +

    Path parameters

    +
    +
    actionId (required)
    + +
    Path Parameter — An identifier for the action. default: null
    spaceId (required)
    + +
    Path Parameter — An identifier for the space. If /s/ and the identifier are omitted from the path, the default space is used. default: null
    +
    + +

    Consumes

    + This API call consumes the following media types via the Content-Type request header: +
      +
    • application/json
    • +
    + +

    Request body

    +
    +
    Legacy_update_connector_request_body_properties Legacy_update_connector_request_body_properties (required)
    + +
    Body Parameter
    + +
    + +

    Request headers

    +
    +
    kbn-xsrf (required)
    + +
    Header Parameter — Cross-site request forgery protection default: null
    + +
    + + + +

    Return type

    + + + + +

    Example data

    +
    Content-Type: application/json
    +
    {
    +  "isPreconfigured" : true,
    +  "isDeprecated" : true,
    +  "actionTypeId" : "actionTypeId",
    +  "name" : "name",
    +  "id" : "id",
    +  "config" : "{}",
    +  "isMissingSecrets" : true
    +}
    + +

    Produces

    + This API call produces the following media types according to the Accept request header; + the media type will be conveyed by the Content-Type response header. +
      +
    • application/json
    • +
    + +

    Responses

    +

    200

    + Indicates a successful call. + action_response_properties +

    404

    + Object is not found. + 404_response +
    +
    Up @@ -350,7 +792,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -421,7 +863,7 @@ Any modifications made to this file will be overwritten.
    kbn-xsrf (required)
    -
    Header Parameter — default: null
    +
    Header Parameter — Cross-site request forgery protection default: null
    @@ -478,12 +920,17 @@ Any modifications made to this file will be overwritten.
  • Create_connector_request_body_properties - Create connector request body properties
  • Get_connector_types_response_body_properties_inner -
  • Get_connectors_response_body_properties - Get connectors response body properties
  • +
  • Legacy_create_connector_request_properties - Legacy create connector request properties
  • +
  • Legacy_get_connector_types_response_body_properties_inner -
  • +
  • Legacy_run_connector_request_body_properties - Legacy run connector request body properties
  • +
  • Legacy_update_connector_request_body_properties - Legacy update connector request body properties
  • Rule_name_mapping - Rule name mapping
  • Run_connector_request_body_properties - Run connector request body properties
  • Run_connector_request_body_properties_params -
  • Severity_mapping - Severity mapping
  • Subaction_parameters - Subaction parameters
  • Update_connector_request_body_properties - Update connector request body properties
  • +
  • action_response_properties - Action response properties
  • config_properties_cases_webhook - Connector request properties for Webhook - Case Management connector
  • config_properties_index - Connector request properties for an index connector
  • config_properties_jira - Connector request properties for a Jira connector
  • @@ -530,6 +977,7 @@ Any modifications made to this file will be overwritten.
  • create_connector_request_xmatters - Create xMatters connector request
  • features -
  • getConnector_404_response -
  • +
  • legacyRunConnector_200_response -
  • runConnector_200_response -
  • runConnector_200_response_data -
  • run_connector_params_documents - Index connector parameters
  • @@ -708,6 +1156,44 @@ Any modifications made to this file will be overwritten.
    referenced_by_count
    Integer Indicates the number of saved objects that reference the connector. If is_preconfigured is true, this value is not calculated.
    +
    +

    Legacy_create_connector_request_properties - Legacy create connector request properties Up

    +
    +
    +
    actionTypeId (optional)
    String The connector type identifier.
    +
    config (optional)
    Object The configuration for the connector. Configuration properties vary depending on the connector type.
    +
    name (optional)
    String The display name for the connector.
    +
    secrets (optional)
    Object The secrets configuration for the connector. Secrets configuration properties vary depending on the connector type. NOTE: Remember these values. You must provide them each time you update the connector.
    +
    +
    +
    +

    Legacy_get_connector_types_response_body_properties_inner - Up

    +
    +
    +
    enabled (optional)
    Boolean Indicates whether the connector type is enabled in Kibana.
    +
    enabledInConfig (optional)
    Boolean Indicates whether the connector type is enabled in the Kibana .yml file.
    +
    enabledInLicense (optional)
    Boolean Indicates whether the connector is enabled in the license.
    +
    id (optional)
    String The unique identifier for the connector type.
    +
    minimumLicenseRequired (optional)
    String The license that is required to use the connector type.
    +
    name (optional)
    String The name of the connector type.
    +
    +
    +
    +

    Legacy_run_connector_request_body_properties - Legacy run connector request body properties Up

    +
    The properties vary depending on the connector type.
    +
    +
    params
    Object The parameters of the connector. Parameter properties vary depending on the connector type.
    +
    +
    +
    +

    Legacy_update_connector_request_body_properties - Legacy update connector request body properties Up

    +
    The properties vary depending on the connector type.
    +
    +
    config (optional)
    Object The new connector configuration. Configuration properties vary depending on the connector type.
    +
    name (optional)
    String The new name for the connector.
    +
    secrets (optional)
    Object The updated secrets configuration for the connector. Secrets properties vary depending on the connector type.
    +
    +

    Rule_name_mapping - Rule name mapping Up

    Mapping for the name of the alert's rule.
    @@ -769,6 +1255,19 @@ Any modifications made to this file will be overwritten.
    secrets
    +
    +

    action_response_properties - Action response properties Up

    +
    The properties vary depending on the action type.
    +
    +
    actionTypeId (optional)
    +
    config (optional)
    +
    id (optional)
    +
    isDeprecated (optional)
    Boolean Indicates whether the action type is deprecated.
    +
    isMissingSecrets (optional)
    Boolean Indicates whether secrets are missing for the action.
    +
    isPreconfigured (optional)
    Boolean Indicates whether it is a preconfigured action.
    +
    name (optional)
    +
    +

    config_properties_cases_webhook - Connector request properties for Webhook - Case Management connector Up

    Defines properties for connectors when type is .cases-webhook.
    @@ -834,7 +1333,7 @@ Any modifications made to this file will be overwritten.
    apiUrl
    String The ServiceNow instance URL.
    clientId (optional)
    String The client ID assigned to your OAuth application. This property is required when isOAuth is true.
    -
    isOAuth (optional)
    String The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    +
    isOAuth (optional)
    Boolean The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    jwtKeyId (optional)
    String The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when isOAuth is true.
    userIdentifierValue (optional)
    String The identifier to use for OAuth authentication. This identifier should be the user field you selected when you created an OAuth JWT API endpoint for external clients in your ServiceNow instance. For example, if the selected user field is Email, the user identifier should be the user's email address. This property is required when isOAuth is true.
    usesTableApi (optional)
    Boolean Determines whether the connector uses the Table API or the Import Set API. This property is supported only for ServiceNow ITSM and ServiceNow SecOps connectors. NOTE: If this property is set to false, the Elastic application should be installed in ServiceNow.
    @@ -846,7 +1345,7 @@ Any modifications made to this file will be overwritten.
    apiUrl
    String The ServiceNow instance URL.
    clientId (optional)
    String The client ID assigned to your OAuth application. This property is required when isOAuth is true.
    -
    isOAuth (optional)
    String The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    +
    isOAuth (optional)
    Boolean The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).
    jwtKeyId (optional)
    String The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when isOAuth is true.
    userIdentifierValue (optional)
    String The identifier to use for OAuth authentication. This identifier should be the user field you selected when you created an OAuth JWT API endpoint for external clients in your ServiceNow instance. For example, if the selected user field is Email, the user identifier should be the user's email address. This property is required when isOAuth is true.
    @@ -1351,6 +1850,15 @@ Any modifications made to this file will be overwritten.
    statusCode (optional)
    +
    +

    legacyRunConnector_200_response - Up

    +
    +
    +
    actionId (optional)
    +
    data (optional)
    +
    status (optional)
    String The status of the action.
    +
    +

    runConnector_200_response - Up

    diff --git a/x-pack/plugins/actions/docs/openapi/bundled.json b/x-pack/plugins/actions/docs/openapi/bundled.json index d887c6de5a3e..cd2df0206b1b 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.json +++ b/x-pack/plugins/actions/docs/openapi/bundled.json @@ -816,6 +816,474 @@ "url": "https://localhost:5601" } ] + }, + "/s/{spaceId}/api/actions/action/{actionId}": { + "delete": { + "summary": "Deletes a connector.", + "operationId": "legacyDeleteConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the delete connector API instead. WARNING: When you delete a connector, it cannot be recovered.\n", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "get": { + "summary": "Retrieves a connector by ID.", + "operationId": "legacyGetConnector", + "description": "Deprecated in 7.13.0. Use the get connector API instead.", + "deprecated": true, + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "put": { + "summary": "Updates the attributes for a connector.", + "operationId": "legacyUpdateConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the update connector API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy update connector request body properties", + "description": "The properties vary depending on the connector type.", + "type": "object", + "properties": { + "config": { + "type": "object", + "description": "The new connector configuration. Configuration properties vary depending on the connector type." + }, + "name": { + "type": "string", + "description": "The new name for the connector." + }, + "secrets": { + "type": "object", + "description": "The updated secrets configuration for the connector. Secrets properties vary depending on the connector type." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + }, + "404": { + "description": "Object is not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/404_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/actions": { + "get": { + "summary": "Retrieves all connectors.", + "operationId": "legacyGetConnectors", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get all connectors API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "post": { + "summary": "Creates a connector.", + "operationId": "legacyCreateConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the create connector API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy create connector request properties", + "type": "object", + "properties": { + "actionTypeId": { + "type": "string", + "description": "The connector type identifier." + }, + "config": { + "type": "object", + "description": "The configuration for the connector. Configuration properties vary depending on the connector type." + }, + "name": { + "type": "string", + "description": "The display name for the connector." + }, + "secrets": { + "type": "object", + "description": "The secrets configuration for the connector. Secrets configuration properties vary depending on the connector type. NOTE: Remember these values. You must provide them each time you update the connector.\n" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/action_response_properties" + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/actions/list_action_types": { + "get": { + "summary": "Retrieves a list of all connector types.", + "operationId": "legacyGetConnectorTypes", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the get all connector types API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "title": "Legacy get connector types response body properties", + "description": "The properties vary for each connector type.", + "type": "array", + "items": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Indicates whether the connector type is enabled in Kibana." + }, + "enabledInConfig": { + "type": "boolean", + "description": "Indicates whether the connector type is enabled in the Kibana `.yml` file." + }, + "enabledInLicense": { + "type": "boolean", + "description": "Indicates whether the connector is enabled in the license.", + "example": true + }, + "id": { + "type": "string", + "description": "The unique identifier for the connector type." + }, + "minimumLicenseRequired": { + "type": "string", + "description": "The license that is required to use the connector type." + }, + "name": { + "type": "string", + "description": "The name of the connector type." + } + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/actions/action/{actionId}/_execute": { + "post": { + "summary": "Runs a connector.", + "operationId": "legacyRunConnector", + "deprecated": true, + "description": "Deprecated in 7.13.0. Use the run connector API instead.", + "tags": [ + "connectors" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/action_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "title": "Legacy run connector request body properties", + "description": "The properties vary depending on the connector type.", + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "type": "object", + "description": "The parameters of the connector. Parameter properties vary depending on the connector type." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "actionId": { + "type": "string" + }, + "data": { + "oneOf": [ + { + "type": "object", + "description": "Information returned from the action.", + "additionalProperties": true + }, + { + "type": "array", + "description": "An array of information returned from the action.", + "items": { + "type": "object" + } + } + ] + }, + "status": { + "type": "string", + "description": "The status of the action." + } + } + } + } + } + }, + "401": { + "description": "Authorization information is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] } }, "components": { @@ -837,6 +1305,7 @@ }, "in": "header", "name": "kbn-xsrf", + "description": "Cross-site request forgery protection", "required": true }, "space_id": { @@ -858,6 +1327,16 @@ "type": "string", "example": "df770e30-8b8b-11ed-a780-3b746c987a81" } + }, + "action_id": { + "in": "path", + "name": "actionId", + "description": "An identifier for the action.", + "required": true, + "schema": { + "type": "string", + "example": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad" + } } }, "schemas": { @@ -1421,7 +1900,7 @@ "isOAuth": { "description": "The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).\n", "default": false, - "type": "string" + "type": "boolean" }, "jwtKeyId": { "description": "The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`.\n", @@ -1516,7 +1995,7 @@ "isOAuth": { "description": "The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth).\n", "default": false, - "type": "string" + "type": "boolean" }, "jwtKeyId": { "description": "The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`.\n", @@ -3779,6 +4258,37 @@ } } } + }, + "action_response_properties": { + "title": "Action response properties", + "description": "The properties vary depending on the action type.", + "type": "object", + "properties": { + "actionTypeId": { + "type": "string" + }, + "config": { + "type": "object" + }, + "id": { + "type": "string" + }, + "isDeprecated": { + "type": "boolean", + "description": "Indicates whether the action type is deprecated." + }, + "isMissingSecrets": { + "type": "boolean", + "description": "Indicates whether secrets are missing for the action." + }, + "isPreconfigured": { + "type": "boolean", + "description": "Indicates whether it is a preconfigured action." + }, + "name": { + "type": "string" + } + } } }, "examples": { diff --git a/x-pack/plugins/actions/docs/openapi/bundled.yaml b/x-pack/plugins/actions/docs/openapi/bundled.yaml index 1652412e027e..6c28df55a358 100644 --- a/x-pack/plugins/actions/docs/openapi/bundled.yaml +++ b/x-pack/plugins/actions/docs/openapi/bundled.yaml @@ -472,6 +472,285 @@ paths: - url: https://localhost:5601 servers: - url: https://localhost:5601 + /s/{spaceId}/api/actions/action/{actionId}: + delete: + summary: Deletes a connector. + operationId: legacyDeleteConnector + deprecated: true + description: | + Deprecated in 7.13.0. Use the delete connector API instead. WARNING: When you delete a connector, it cannot be recovered. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + get: + summary: Retrieves a connector by ID. + operationId: legacyGetConnector + description: Deprecated in 7.13.0. Use the get connector API instead. + deprecated: true + tags: + - connectors + parameters: + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/action_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + put: + summary: Updates the attributes for a connector. + operationId: legacyUpdateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the update connector API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update connector request body properties + description: The properties vary depending on the connector type. + type: object + properties: + config: + type: object + description: The new connector configuration. Configuration properties vary depending on the connector type. + name: + type: string + description: The new name for the connector. + secrets: + type: object + description: The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/action_response_properties' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '#/components/schemas/404_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/actions: + get: + summary: Retrieves all connectors. + operationId: legacyGetConnectors + deprecated: true + description: Deprecated in 7.13.0. Use the get all connectors API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/action_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + post: + summary: Creates a connector. + operationId: legacyCreateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the create connector API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create connector request properties + type: object + properties: + actionTypeId: + type: string + description: The connector type identifier. + config: + type: object + description: The configuration for the connector. Configuration properties vary depending on the connector type. + name: + type: string + description: The display name for the connector. + secrets: + type: object + description: | + The secrets configuration for the connector. Secrets configuration properties vary depending on the connector type. NOTE: Remember these values. You must provide them each time you update the connector. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '#/components/schemas/action_response_properties' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/actions/list_action_types: + get: + summary: Retrieves a list of all connector types. + operationId: legacyGetConnectorTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get all connector types API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + title: Legacy get connector types response body properties + description: The properties vary for each connector type. + type: array + items: + type: object + properties: + enabled: + type: boolean + description: Indicates whether the connector type is enabled in Kibana. + enabledInConfig: + type: boolean + description: Indicates whether the connector type is enabled in the Kibana `.yml` file. + enabledInLicense: + type: boolean + description: Indicates whether the connector is enabled in the license. + example: true + id: + type: string + description: The unique identifier for the connector type. + minimumLicenseRequired: + type: string + description: The license that is required to use the connector type. + name: + type: string + description: The name of the connector type. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/actions/action/{actionId}/_execute: + post: + summary: Runs a connector. + operationId: legacyRunConnector + deprecated: true + description: Deprecated in 7.13.0. Use the run connector API instead. + tags: + - connectors + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/action_id' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy run connector request body properties + description: The properties vary depending on the connector type. + type: object + required: + - params + properties: + params: + type: object + description: The parameters of the connector. Parameter properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + actionId: + type: string + data: + oneOf: + - type: object + description: Information returned from the action. + additionalProperties: true + - type: array + description: An array of information returned from the action. + items: + type: object + status: + type: string + description: The status of the action. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 components: securitySchemes: basicAuth: @@ -487,6 +766,7 @@ components: type: string in: header name: kbn-xsrf + description: Cross-site request forgery protection required: true space_id: in: path @@ -504,6 +784,14 @@ components: schema: type: string example: df770e30-8b8b-11ed-a780-3b746c987a81 + action_id: + in: path + name: actionId + description: An identifier for the action. + required: true + schema: + type: string + example: c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad schemas: config_properties_cases_webhook: title: Connector request properties for Webhook - Case Management connector @@ -945,7 +1233,7 @@ components: description: | The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: | The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`. @@ -1022,7 +1310,7 @@ components: description: | The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: | The key identifier assigned to the JWT verifier map of your OAuth application. This property is required when `isOAuth` is `true`. @@ -2671,6 +2959,28 @@ components: urgency: type: string description: The urgency of the incident for ServiceNow ITSM connectors. + action_response_properties: + title: Action response properties + description: The properties vary depending on the action type. + type: object + properties: + actionTypeId: + type: string + config: + type: object + id: + type: string + isDeprecated: + type: boolean + description: Indicates whether the action type is deprecated. + isMissingSecrets: + type: boolean + description: Indicates whether secrets are missing for the action. + isPreconfigured: + type: boolean + description: Indicates whether it is a preconfigured action. + name: + type: string examples: create_index_connector_request: summary: Create an index connector. diff --git a/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml b/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml index 3d8dfae634e6..fe0402a43aa0 100644 --- a/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/headers/kbn_xsrf.yaml @@ -2,4 +2,5 @@ schema: type: string in: header name: kbn-xsrf +description: Cross-site request forgery protection required: true diff --git a/x-pack/plugins/actions/docs/openapi/components/parameters/action_id.yaml b/x-pack/plugins/actions/docs/openapi/components/parameters/action_id.yaml new file mode 100644 index 000000000000..3ee0b642c9de --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/components/parameters/action_id.yaml @@ -0,0 +1,7 @@ +in: path +name: actionId +description: An identifier for the action. +required: true +schema: + type: string + example: c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/action_response_properties.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/action_response_properties.yaml new file mode 100644 index 000000000000..ccc6b4bf8a46 --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/action_response_properties.yaml @@ -0,0 +1,21 @@ +title: Action response properties +description: The properties vary depending on the action type. +type: object +properties: + actionTypeId: + type: string + config: + type: object + id: + type: string + isDeprecated: + type: boolean + description: Indicates whether the action type is deprecated. + isMissingSecrets: + type: boolean + description: Indicates whether secrets are missing for the action. + isPreconfigured: + type: boolean + description: Indicates whether it is a preconfigured action. + name: + type: string \ No newline at end of file diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml index f7013535f2e5..702a738d436a 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow.yaml @@ -17,7 +17,7 @@ properties: The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: > The key identifier assigned to the JWT verifier map of your OAuth application. diff --git a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml index f35f96629c86..34aa025298b5 100644 --- a/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml +++ b/x-pack/plugins/actions/docs/openapi/components/schemas/config_properties_servicenow_itom.yaml @@ -17,7 +17,7 @@ properties: The type of authentication to use. The default value is false, which means basic authentication is used instead of open authorization (OAuth). default: false - type: string + type: boolean jwtKeyId: description: > The key identifier assigned to the JWT verifier map of your OAuth application. diff --git a/x-pack/plugins/actions/docs/openapi/entrypoint.yaml b/x-pack/plugins/actions/docs/openapi/entrypoint.yaml index 579845aa9f6d..04e844607f82 100644 --- a/x-pack/plugins/actions/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/actions/docs/openapi/entrypoint.yaml @@ -26,16 +26,14 @@ paths: '/s/{spaceId}/api/actions/connector/{connectorId}/_execute': $ref: paths/s@{spaceid}@api@actions@connector@{connectorid}@_execute.yaml # Deprecated endpoints: -# '/s/{spaceId}/api/actions/action/{actionId}': -# $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}.yaml' -# '/s/{spaceId}/api/actions': -# $ref: 'paths/s@{spaceid}@api@actions.yaml' -# '/s/{spaceId}/api/actions/list_action_types': -# $ref: 'paths/s@{spaceid}@api@actions@list_action_types.yaml' -# '/s/{spaceId}/api/actions/action': -# $ref: 'paths/s@{spaceid}@api@actions@action.yaml' -# '/s/{spaceId}/api/actions/action/{actionId}/_execute': -# $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml' + '/s/{spaceId}/api/actions/action/{actionId}': + $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}.yaml' + '/s/{spaceId}/api/actions': + $ref: 'paths/s@{spaceid}@api@actions.yaml' + '/s/{spaceId}/api/actions/list_action_types': + $ref: 'paths/s@{spaceid}@api@actions@list_action_types.yaml' + '/s/{spaceId}/api/actions/action/{actionId}/_execute': + $ref: 'paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml' components: securitySchemes: basicAuth: diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions.yaml new file mode 100644 index 000000000000..6b697dca8c4c --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions.yaml @@ -0,0 +1,78 @@ +get: + summary: Retrieves all connectors. + operationId: legacyGetConnectors + deprecated: true + description: Deprecated in 7.13.0. Use the get all connectors API instead. + tags: + - connectors + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: array + items: + $ref: '../components/schemas/action_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +post: + summary: Creates a connector. + operationId: legacyCreateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the create connector API instead. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy create connector request properties + type: object + properties: + actionTypeId: + type: string + description: The connector type identifier. + config: + type: object + description: The configuration for the connector. Configuration properties vary depending on the connector type. + name: + type: string + description: The display name for the connector. + secrets: + type: object + description: > + The secrets configuration for the connector. + Secrets configuration properties vary depending on the connector type. + NOTE: Remember these values. You must provide them each time you update the connector. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/action_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 + diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}.yaml new file mode 100644 index 000000000000..42b0b90a8e48 --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}.yaml @@ -0,0 +1,98 @@ +delete: + summary: Deletes a connector. + operationId: legacyDeleteConnector + deprecated: true + description: > + Deprecated in 7.13.0. Use the delete connector API instead. + WARNING: When you delete a connector, it cannot be recovered. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + responses: + '204': + description: Indicates a successful call. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +get: + summary: Retrieves a connector by ID. + operationId: legacyGetConnector + description: Deprecated in 7.13.0. Use the get connector API instead. + deprecated: true + tags: + - connectors + parameters: + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/action_response_properties.yaml' + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 + +put: + summary: Updates the attributes for a connector. + operationId: legacyUpdateConnector + deprecated: true + description: Deprecated in 7.13.0. Use the update connector API instead. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy update connector request body properties + description: The properties vary depending on the connector type. + type: object + properties: + config: + type: object + description: The new connector configuration. Configuration properties vary depending on the connector type. + name: + type: string + description: The new name for the connector. + secrets: + type: object + description: The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../components/schemas/action_response_properties.yaml' + '404': + description: Object is not found. + content: + application/json: + schema: + $ref: '../components/schemas/404_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml new file mode 100644 index 000000000000..d7a9f5caf090 --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@action@{actionid}@_execute.yaml @@ -0,0 +1,57 @@ +post: + summary: Runs a connector. + operationId: legacyRunConnector + deprecated: true + description: Deprecated in 7.13.0. Use the run connector API instead. + tags: + - connectors + parameters: + - $ref: '../components/headers/kbn_xsrf.yaml' + - $ref: '../components/parameters/action_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + requestBody: + required: true + content: + application/json: + schema: + title: Legacy run connector request body properties + description: The properties vary depending on the connector type. + type: object + required: + - params + properties: + params: + type: object + description: The parameters of the connector. Parameter properties vary depending on the connector type. + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + type: object + properties: + actionId: + type: string + data: + oneOf: + - type: object + description: Information returned from the action. + additionalProperties: true + - type: array + description: An array of information returned from the action. + items: + type: object + status: + type: string + description: The status of the action. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 diff --git a/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@list_action_types.yaml b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@list_action_types.yaml new file mode 100644 index 000000000000..3ad21728ccc7 --- /dev/null +++ b/x-pack/plugins/actions/docs/openapi/paths/s@{spaceid}@api@actions@list_action_types.yaml @@ -0,0 +1,50 @@ +get: + summary: Retrieves a list of all connector types. + operationId: legacyGetConnectorTypes + deprecated: true + description: Deprecated in 7.13.0. Use the get all connector types API instead. + tags: + - connectors + parameters: + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json: + schema: + title: Legacy get connector types response body properties + description: The properties vary for each connector type. + type: array + items: + type: object + properties: + enabled: + type: boolean + description: Indicates whether the connector type is enabled in Kibana. + enabledInConfig: + type: boolean + description: Indicates whether the connector type is enabled in the Kibana `.yml` file. + enabledInLicense: + type: boolean + description: Indicates whether the connector is enabled in the license. + example: true + id: + type: string + description: The unique identifier for the connector type. + minimumLicenseRequired: + type: string + description: The license that is required to use the connector type. + name: + type: string + description: The name of the connector type. + '401': + description: Authorization information is missing or invalid. + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 From 6e6cd7fae65bf856cd2db0cbe5e334bd8005f53c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 2 Feb 2023 10:23:39 -0500 Subject: [PATCH 17/35] [Fleet] Update write indices on experimental datastream settings change (#149967) --- .../experimental_datastream_features.test.ts | 102 +++++++++++++++--- .../experimental_datastream_features.ts | 35 ++++-- .../apis/epm/data_stream.ts | 90 ++++++++++++++++ .../data_stream/test_logs/fields/fields.yml | 4 + .../test_metrics/fields/fields.yml | 4 + 5 files changed, 213 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts index db37f85f5670..500cf141fed2 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.test.ts @@ -9,10 +9,15 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; import type { NewPackagePolicy, PackagePolicy } from '../../types'; +import { updateCurrentWriteIndices } from '../epm/elasticsearch/template/template'; import { getInstallation } from '../epm/packages'; import { handleExperimentalDatastreamFeatureOptIn } from './experimental_datastream_features'; +const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction< + typeof updateCurrentWriteIndices +>; + jest.mock('../epm/packages', () => { return { getInstallation: jest.fn(), @@ -27,6 +32,9 @@ jest.mock('../epm/packages', () => { }; }); +jest.mock('../app_context'); +jest.mock('../epm/elasticsearch/template/template'); + const mockGetInstallation = getInstallation as jest.Mock; jest.mock('../epm/elasticsearch/template/install', () => { @@ -140,6 +148,7 @@ function getExistingTestPackagePolicy({ describe('experimental_datastream_features', () => { beforeEach(() => { soClient.get.mockClear(); + mockedUpdateCurrentWriteIndices.mockReset(); esClient.cluster.getComponentTemplate.mockClear(); esClient.cluster.putComponentTemplate.mockClear(); @@ -173,6 +182,24 @@ describe('experimental_datastream_features', () => { }, ], }); + + esClient.indices.getIndexTemplate.mockResolvedValueOnce({ + index_templates: [ + { + name: 'metrics-test.test', + index_template: { + template: { + settings: {}, + mappings: {}, + }, + composed_of: [], + index_patterns: '', + }, + }, + ], + }); + + esClient.indices.getIndexTemplate.mockClear(); }); const soClient = savedObjectsClientMock.create(); @@ -310,22 +337,6 @@ describe('experimental_datastream_features', () => { isDocValueOnlyOther: false, }); - esClient.indices.getIndexTemplate.mockResolvedValueOnce({ - index_templates: [ - { - name: 'metrics-test.test', - index_template: { - template: { - settings: {}, - mappings: {}, - }, - composed_of: [], - index_patterns: '', - }, - }, - ], - }); - await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); expect(esClient.indices.getIndexTemplate).toHaveBeenCalled(); @@ -372,6 +383,33 @@ describe('experimental_datastream_features', () => { expect(esClient.cluster.getComponentTemplate).not.toHaveBeenCalled(); expect(esClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); }); + + it('does not update write indices', async () => { + const packagePolicy = getExistingTestPackagePolicy({ + isSyntheticSourceEnabled: true, + isTSDBEnabled: false, + isDocValueOnlyNumeric: false, + isDocValueOnlyOther: false, + }); + + mockGetInstallation.mockResolvedValueOnce({ + experimental_data_stream_features: [ + { + data_stream: 'metrics-test.test', + features: { + synthetic_source: true, + tsdb: false, + doc_value_only_numeric: false, + doc_value_only_other: false, + }, + }, + ], + }); + + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + + expect(mockedUpdateCurrentWriteIndices).not.toHaveBeenCalled(); + }); }); describe('when opt in status is changed', () => { @@ -509,6 +547,38 @@ describe('experimental_datastream_features', () => { }) ); }); + + it('should update existing write indices', async () => { + const packagePolicy = getExistingTestPackagePolicy({ + isSyntheticSourceEnabled: false, + isTSDBEnabled: true, + isDocValueOnlyNumeric: false, + isDocValueOnlyOther: false, + }); + + esClient.indices.getIndexTemplate.mockResolvedValueOnce({ + index_templates: [ + { + name: 'metrics-test.test', + index_template: { + template: { + settings: {}, + mappings: {}, + }, + composed_of: [], + index_patterns: '', + }, + }, + ], + }); + + await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy }); + + expect(mockedUpdateCurrentWriteIndices).toHaveBeenCalledTimes(1); + expect( + mockedUpdateCurrentWriteIndices.mock.calls[0][2].map(({ templateName }) => templateName) + ).toEqual(['metrics-test.test']); + }); }); }); }); diff --git a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts index 88bde17e2aa3..e98f45a5671c 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/experimental_datastream_features.ts @@ -13,8 +13,15 @@ import { merge } from 'lodash'; import { getRegistryDataStreamAssetBaseName } from '../../../common/services'; import type { ExperimentalIndexingFeature } from '../../../common/types'; -import type { NewPackagePolicy, PackagePolicy } from '../../types'; +import type { + NewPackagePolicy, + PackagePolicy, + IndexTemplate, + IndexTemplateEntry, +} from '../../types'; +import { appContextService } from '../app_context'; import { prepareTemplate } from '../epm/elasticsearch/template/install'; +import { updateCurrentWriteIndices } from '../epm/elasticsearch/template/template'; import { getInstallation, getPackageInfo } from '../epm/packages'; import { updateDatastreamExperimentalFeatures } from '../epm/packages/update'; import { @@ -71,6 +78,8 @@ export async function handleExperimentalDatastreamFeatureOptIn({ }); } + const updatedIndexTemplates: IndexTemplateEntry[] = []; + for (const featureMapEntry of packagePolicy.package.experimental_data_stream_features) { const existingOptIn = installation?.experimental_data_stream_features?.find( (optIn) => optIn.data_stream === featureMapEntry.data_stream @@ -126,6 +135,10 @@ export async function handleExperimentalDatastreamFeatureOptIn({ let sourceModeSettings = {}; + const indexTemplateRes = await esClient.indices.getIndexTemplate({ + name: featureMapEntry.data_stream, + }); + if (isSyntheticSourceOptInChanged) { sourceModeSettings = { _source: { @@ -152,12 +165,10 @@ export async function handleExperimentalDatastreamFeatureOptIn({ }); } - if (isTSDBOptInChanged) { - const indexTemplateRes = await esClient.indices.getIndexTemplate({ - name: featureMapEntry.data_stream, - }); - const indexTemplate = indexTemplateRes.index_templates[0].index_template; + const indexTemplate = indexTemplateRes.index_templates[0].index_template; + let updatedIndexTemplate = indexTemplate as IndexTemplate; + if (isTSDBOptInChanged) { const indexTemplateBody = { ...indexTemplate, template: { @@ -171,12 +182,24 @@ export async function handleExperimentalDatastreamFeatureOptIn({ }, }; + updatedIndexTemplate = indexTemplateBody as IndexTemplate; + await esClient.indices.putIndexTemplate({ name: featureMapEntry.data_stream, // @ts-expect-error body: indexTemplateBody, }); } + + updatedIndexTemplates.push({ + templateName: featureMapEntry.data_stream, + indexTemplate: updatedIndexTemplate, + }); + } + + // Trigger rollover for updated datastreams + if (updatedIndexTemplates.length > 0) { + await updateCurrentWriteIndices(esClient, appContextService.getLogger(), updatedIndexTemplates); } // Update the installation object to persist the experimental feature map diff --git a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts index 466f150ec1e0..2e5197f4543c 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/data_stream.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { v4 as uuidv4 } from 'uuid'; import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -167,5 +168,94 @@ export default function (providerContext: FtrProviderContext) { }); await installPackage(pkgName, pkgUpdateVersion); }); + + describe('When enabling experimental data stream features', () => { + let agentPolicyId: string; + let packagePolicyId: string; + + let packagePolicyData: any; + + beforeEach(async () => { + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: `Test policy ${uuidv4()}`, + namespace: 'default', + }) + .expect(200); + agentPolicyId = agentPolicyResponse.item.id; + packagePolicyData = { + force: true, + name: `test-package-experimental-feature-${uuidv4()}`, + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + inputs: [], + package: { + name: pkgName, + version: pkgVersion, + }, + }; + const { body: responseWithForce } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send(packagePolicyData) + .expect(200); + + packagePolicyId = responseWithForce.item.id; + }); + afterEach(async () => { + await supertest + .post(`/api/fleet/agent_policies/delete`) + .send({ + agentPolicyId, + }) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + async function getLogsDefaultBackingIndicesLength() { + const resLogsDatastream = await es.transport.request( + { + method: 'GET', + path: `/_data_stream/${logsTemplateName}-${namespaces[0]}`, + }, + { meta: true } + ); + + return resLogsDatastream.body.data_streams[0].indices.length; + } + + it('should rollover datstream after enabling a expiremental datastream feature that need a rollover', async () => { + expect(await getLogsDefaultBackingIndicesLength()).to.be(1); + + await supertest + .put(`/api/fleet/package_policies/${packagePolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + ...packagePolicyData, + package: { + ...packagePolicyData.package, + experimental_data_stream_features: [ + { + data_stream: logsTemplateName, + features: { + synthetic_source: false, + tsdb: false, + doc_value_only_numeric: true, + doc_value_only_other: true, + }, + }, + ], + }, + }) + .expect(200); + + // Datastream should have been rolled over + expect(await getLogsDefaultBackingIndicesLength()).to.be(2); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml index 6e003ed0ad14..928d5a4a426c 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_logs/fields/fields.yml @@ -10,6 +10,10 @@ type: constant_keyword description: > Data stream namespace. +- name: numeric_field + type: integer + description: > + Numeric field - name: '@timestamp' type: date description: > diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml index 6e003ed0ad14..928d5a4a426c 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/datastreams/0.1.0/data_stream/test_metrics/fields/fields.yml @@ -10,6 +10,10 @@ type: constant_keyword description: > Data stream namespace. +- name: numeric_field + type: integer + description: > + Numeric field - name: '@timestamp' type: date description: > From 39d941e35b823c8583dfa879e1588e233959cda7 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 2 Feb 2023 10:24:14 -0500 Subject: [PATCH 18/35] [Fleet] Fix metrics flaky test (#150119) --- x-pack/test/fleet_api_integration/apis/agents/list.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index 05a2f25cfc4e..fde30c9185be 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -17,8 +17,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); let elasticAgentpkgVersion: string; - // FLAKY: https://github.com/elastic/kibana/issues/149937 - describe.skip('fleet_list_agent', () => { + describe('fleet_list_agent', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/fleet/agents'); const getPkRes = await supertest @@ -131,11 +130,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return metrics if available and called with withMetrics', async () => { + const now = Date.now(); await es.index({ index: 'metrics-elastic_agent.elastic_agent-default', refresh: 'wait_for', document: { - '@timestamp': new Date(Date.now() - 3 * 60 * 1000).toISOString(), + '@timestamp': new Date(now - 2 * 60 * 1000).toISOString(), data_stream: { namespace: 'default', type: 'metrics', @@ -160,7 +160,7 @@ export default function ({ getService }: FtrProviderContext) { index: 'metrics-elastic_agent.elastic_agent-default', refresh: 'wait_for', document: { - '@timestamp': new Date(Date.now() - 2 * 60 * 1000).toISOString(), + '@timestamp': new Date(now - 1 * 60 * 1000).toISOString(), elastic_agent: { id: 'agent1', process: 'elastic_agent' }, data_stream: { namespace: 'default', From b1617498a5f7866e6cd625641b785bab756376f6 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 2 Feb 2023 17:49:30 +0200 Subject: [PATCH 19/35] Mark spaces as optional on the contracts (#150021) ## Summary Part of https://github.com/elastic/kibana/issues/149687 Lens defines the space plugin as optional. I am fixing the types here in order to be also optional on the contract. --- x-pack/plugins/lens/public/app_plugin/app.test.tsx | 2 +- x-pack/plugins/lens/public/app_plugin/types.ts | 2 +- x-pack/plugins/lens/public/plugin.ts | 2 +- .../plugins/lens/public/state_management/load_initial.test.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index aa815e833a6d..73afa216e62d 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -1607,7 +1607,7 @@ describe('Lens App', () => { }, }, }); - expect(services.spaces.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + expect(services.spaces?.ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ currentObjectId: '1234', objectNoun: 'Lens visualization', otherObjectId: '2', diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 314bc3a2e52f..193ba130e02e 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -158,7 +158,7 @@ export interface LensAppServices { savedObjectsTagging?: SavedObjectTaggingPluginStart; getOriginatingAppName: () => string | undefined; presentationUtil: PresentationUtilPluginStart; - spaces: SpacesApi; + spaces?: SpacesApi; charts: ChartsPluginSetup; share?: SharePluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 6db5d4abb90e..2e269f37e7f8 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -146,7 +146,7 @@ export interface LensPluginStartDependencies { dataViewFieldEditor: IndexPatternFieldEditorStart; dataViewEditor: DataViewEditorStart; inspector: InspectorStartContract; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; usageCollection?: UsageCollectionStart; docLinks: DocLinksStart; share?: SharePluginStart; diff --git a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx index 2d8ce9405f12..ecf519382ff6 100644 --- a/x-pack/plugins/lens/public/state_management/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/load_initial.test.tsx @@ -309,7 +309,7 @@ describe('Initializing the store', () => { expect(deps.lensServices.attributeService.unwrapAttributes).toHaveBeenCalledWith({ savedObjectId: defaultSavedObjectId, }); - expect(deps.lensServices.spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith({ + expect(deps.lensServices.spaces?.ui.redirectLegacyUrl).toHaveBeenCalledWith({ path: '#/edit/id2?search', aliasPurpose: 'savedObjectConversion', objectNoun: 'Lens visualization', From df809bd53a4ea10b94c894774e1ac6b0c4c3e2e2 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 2 Feb 2023 17:02:38 +0100 Subject: [PATCH 20/35] [Saved objects] Prepare SO management for versionable types (#149495) ## Summary Part of preparing HTTP APIs and associated interfaces for versioning: * Add domain-specific interfaces to the saved object management plugin * Add a V1 interface of domain types to `common` * Remove use of deprecated `SavedObject` type from public * Follows on from https://github.com/elastic/kibana/pull/148602 Related https://github.com/elastic/kibana/issues/149098 Fixes https://github.com/elastic/kibana/pull/149495 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../saved_objects_management/common/index.ts | 2 +- .../saved_objects_management/common/types.ts | 61 ------ .../common/types/README.md | 58 ++++++ .../common/types/index.ts | 19 ++ .../types/latest.ts} | 6 +- .../common/types/v1.ts | 178 ++++++++++++++++++ .../public/lib/bulk_delete_objects.ts | 24 +-- .../public/lib/bulk_get_objects.ts | 15 +- .../public/lib/case_conversion.test.ts | 25 --- .../lib/fetch_export_by_type_and_search.ts | 2 +- .../public/lib/find_objects.ts | 35 +--- .../public/lib/get_allowed_types.ts | 12 +- .../public/lib/get_relationships.test.ts | 4 +- .../public/lib/get_relationships.ts | 8 +- .../public/lib/get_saved_object_counts.ts | 12 +- .../lib/process_import_response.test.ts | 2 +- .../public/lib/process_import_response.ts | 2 +- .../public/lib/resolve_import_errors.ts | 2 +- .../components/relationships.tsx | 4 +- .../saved_objects_table.test.tsx | 3 +- .../objects_table/saved_objects_table.tsx | 3 +- .../saved_objects_management/public/plugin.ts | 6 +- .../saved_objects_management/public/types.ts | 1 - .../server/lib/find_relationships.ts | 9 +- .../server/lib/index.ts | 1 + .../server/lib/to_saved_object_with_meta.ts | 24 +++ .../server/routes/bulk_delete.ts | 4 +- .../server/routes/bulk_get.ts | 17 +- .../server/routes/find.ts | 33 ++-- .../server/routes/get_allowed_types.ts | 4 +- .../server/routes/relationships.ts | 7 +- .../server/routes/scroll_count.ts | 7 +- .../saved_objects_management/server/types.ts | 1 - .../saved_objects_management/tsconfig.json | 1 - .../visible_in_management.ts | 2 +- 35 files changed, 383 insertions(+), 211 deletions(-) delete mode 100644 src/plugins/saved_objects_management/common/types.ts create mode 100644 src/plugins/saved_objects_management/common/types/README.md create mode 100644 src/plugins/saved_objects_management/common/types/index.ts rename src/plugins/saved_objects_management/{public/lib/case_conversion.ts => common/types/latest.ts} (66%) create mode 100644 src/plugins/saved_objects_management/common/types/v1.ts delete mode 100644 src/plugins/saved_objects_management/public/lib/case_conversion.test.ts create mode 100644 src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts diff --git a/src/plugins/saved_objects_management/common/index.ts b/src/plugins/saved_objects_management/common/index.ts index bc4631d2c8e6..895d21610d88 100644 --- a/src/plugins/saved_objects_management/common/index.ts +++ b/src/plugins/saved_objects_management/common/index.ts @@ -12,6 +12,6 @@ export type { SavedObjectRelation, SavedObjectRelationKind, SavedObjectInvalidRelation, - SavedObjectGetRelationshipsResponse, SavedObjectManagementTypeInfo, + v1, } from './types'; diff --git a/src/plugins/saved_objects_management/common/types.ts b/src/plugins/saved_objects_management/common/types.ts deleted file mode 100644 index 957fec6a87e5..000000000000 --- a/src/plugins/saved_objects_management/common/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { SavedObject } from '@kbn/core/types'; -import type { SavedObjectsNamespaceType } from '@kbn/core/public'; - -/** - * The metadata injected into a {@link SavedObject | saved object} when returning - * {@link SavedObjectWithMetadata | enhanced objects} from the plugin API endpoints. - */ -export interface SavedObjectMetadata { - icon?: string; - title?: string; - editUrl?: string; - inAppUrl?: { path: string; uiCapabilitiesPath: string }; - namespaceType?: SavedObjectsNamespaceType; - hiddenType?: boolean; -} - -/** - * A {@link SavedObject | saved object} enhanced with meta properties used by the client-side plugin. - */ -export type SavedObjectWithMetadata = SavedObject & { - meta: SavedObjectMetadata; -}; - -export type SavedObjectRelationKind = 'child' | 'parent'; - -/** - * Represents a relation between two {@link SavedObject | saved object} - */ -export interface SavedObjectRelation { - id: string; - type: string; - relationship: SavedObjectRelationKind; - meta: SavedObjectMetadata; -} - -export interface SavedObjectInvalidRelation { - id: string; - type: string; - relationship: SavedObjectRelationKind; - error: string; -} - -export interface SavedObjectGetRelationshipsResponse { - relations: SavedObjectRelation[]; - invalidRelations: SavedObjectInvalidRelation[]; -} - -export interface SavedObjectManagementTypeInfo { - name: string; - namespaceType: SavedObjectsNamespaceType; - hidden: boolean; - displayName: string; -} diff --git a/src/plugins/saved_objects_management/common/types/README.md b/src/plugins/saved_objects_management/common/types/README.md new file mode 100644 index 000000000000..bb8ccccaa3f2 --- /dev/null +++ b/src/plugins/saved_objects_management/common/types/README.md @@ -0,0 +1,58 @@ +## Versioned interfaces + +This folder contains types that are shared between the server and client: + +```ts +// v1.ts +export interface SavedObjectWithMetadata { name: string } + +// index.ts +import * as v1 from './v1'; +export type { v1 }; + +// Used elsewhere +import type { v1 } from '../common'; +const myObject: v1.SavedObjectWithMetadata = { name: 'my object' }; +``` + +**Do not alter a versioned type**. Types may be in use by clients (if the code is released). +Alterations must be made on a new version of the TS interface. + +## Create a new version + +Versions in this plugin are determined using monotonically increasing numbers: 1, 2, 3, etc. + +1. Find the latest version, e.g: `v2`. +2. Create a new file, e.g., `v3.ts` if it does not exist. +3. Copy the type(s) to change from previous version. E.g. `v2.ts`'s `SavedObjectWithMetadata`. +4. Alter the interface as needed +5. Re-export `v2` types to "inherit" the entire previous version's types: `export * from './v2';` +6. Export your new version from latest: `export * from './v3';`. This may result in TS errors + to be fixed. +7. Export your new file from index.ts as `v3`. + +Your `v3.ts` file should look something like: + +```ts +export * from './v3'; +export interface SavedObjectWithMetadata { name: string; a_new_field: string; } +``` + +In this way the entire API is accessible from `v3` including types that may +not have changed. + +Any alterations post-release must be in a new version (start at step 1). + + +## The `latest.ts` file + +The `latest.ts` file is a container for all "latest" versions of types. This is useful +for app code that always needs the latest version of your interfaces. E.g.: + +```ts +import type { SavedObjectWithMetadata } from '../common'; +``` + +Notice that there is no version number mentioned. Either in the interface name +or import path. To update the "latest" type you must re-export the new version +from the appropriate versioned path. diff --git a/src/plugins/saved_objects_management/common/types/index.ts b/src/plugins/saved_objects_management/common/types/index.ts new file mode 100644 index 000000000000..8e6e27b97715 --- /dev/null +++ b/src/plugins/saved_objects_management/common/types/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +export type { + SavedObjectInvalidRelation, + SavedObjectManagementTypeInfo, + SavedObjectMetadata, + SavedObjectRelation, + SavedObjectRelationKind, + SavedObjectWithMetadata, +} from './latest'; + +import type * as v1 from './v1'; +export type { v1 }; diff --git a/src/plugins/saved_objects_management/public/lib/case_conversion.ts b/src/plugins/saved_objects_management/common/types/latest.ts similarity index 66% rename from src/plugins/saved_objects_management/public/lib/case_conversion.ts rename to src/plugins/saved_objects_management/common/types/latest.ts index d05955cc7c77..e9c79f0f50f9 100644 --- a/src/plugins/saved_objects_management/public/lib/case_conversion.ts +++ b/src/plugins/saved_objects_management/common/types/latest.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -import { mapKeys, camelCase } from 'lodash'; - -export function keysToCamelCaseShallow(object: Record) { - return mapKeys(object, (value, key) => camelCase(key)); -} +export * from './v1'; diff --git a/src/plugins/saved_objects_management/common/types/v1.ts b/src/plugins/saved_objects_management/common/types/v1.ts new file mode 100644 index 000000000000..86b2486c0872 --- /dev/null +++ b/src/plugins/saved_objects_management/common/types/v1.ts @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectError } from '@kbn/core/types'; +import type { SavedObjectsNamespaceType } from '@kbn/core/public'; + +/** Domain interfaces */ + +/** + * Saved Object Management metadata associated with a saved object. See + * {@link SavedObjectWithMetadata}. + */ +export interface SavedObjectMetadata { + icon?: string; + title?: string; + editUrl?: string; + inAppUrl?: { path: string; uiCapabilitiesPath: string }; + namespaceType?: SavedObjectsNamespaceType; + hiddenType?: boolean; +} + +/** + * One saved object's reference to another saved object. + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +/** + * A saved object. + * + * @note This is intended as a domain-specific representation of a SavedObject + * which is intended for server-side only use. + */ +export interface SavedObjectWithMetadata { + id: string; + type: string; + meta: SavedObjectMetadata; + error?: SavedObjectError; + created_at?: string; + updated_at?: string; + attributes: T; + namespaces?: string[]; + references: SavedObjectReference[]; +} + +export type SavedObjectRelationKind = 'child' | 'parent'; + +/** + * Represents a relation between two {@link SavedObjectWithMetadata | saved objects}. + */ +export interface SavedObjectRelation { + id: string; + type: string; + relationship: SavedObjectRelationKind; + meta: SavedObjectMetadata; +} + +/** + * Represents a relation between two {@link SavedObjectWithMetadata | saved objects}. + */ +export interface SavedObjectInvalidRelation { + id: string; + type: string; + relationship: SavedObjectRelationKind; + error: string; +} + +export interface SavedObjectManagementTypeInfo { + name: string; + // TODO: Fix. We should not directly expose these values to public code. + namespaceType: SavedObjectsNamespaceType; + hidden: boolean; + displayName: string; +} + +/** HTTP API interfaces */ + +export type BulkGetBodyHTTP = Array<{ + id: string; + type: string; +}>; + +export type BulkGetResponseHTTP = SavedObjectWithMetadata[]; + +export type BulkDeleteBodyHTTP = Array<{ + type: string; + id: string; +}>; + +export type BulkDeleteResponseHTTP = Array<{ + /** The ID of the saved object */ + id: string; + /** The type of the saved object */ + type: string; + /** The status of deleting the object: true for deleted, false for error */ + success: boolean; + /** Reason the object could not be deleted (success is false) */ + error?: SavedObjectError; +}>; + +export type FindSearchOperatorHTTP = 'AND' | 'OR'; +export type FindSortOrderHTTP = 'asc' | 'desc'; + +export interface ReferenceHTTP { + type: string; + id: string; +} + +export interface FindQueryHTTP { + perPage?: number; + page?: number; + type: string | string[]; + // TODO: Fix. this API allows writing an arbitrary query that is passed straight to our persistence layer, thus leaking SO attributes to the public... + search?: string; + defaultSearchOperator?: FindSearchOperatorHTTP; + // TODO: Fix. this API allows sorting by any field, thus leaking SO attributes to the public... + sortField?: string; + sortOrder?: FindSortOrderHTTP; + hasReference?: ReferenceHTTP | ReferenceHTTP[]; + hasReferenceOperator?: FindSearchOperatorHTTP; + // TODO: Fix. This exposes attribute schemas to clients. + fields?: string | string[]; +} + +export interface FindResponseHTTP { + saved_objects: SavedObjectWithMetadata[]; + total: number; + page: number; + per_page: number; +} + +export interface GetAllowedTypesResponseHTTP { + types: SavedObjectManagementTypeInfo[]; +} + +export interface RelationshipsParamsHTTP { + type: string; + id: string; +} + +export interface RelationshipsQueryHTTP { + size: number; + savedObjectTypes: string | string[]; +} + +export interface RelationshipsResponseHTTP { + relations: SavedObjectRelation[]; + invalidRelations: SavedObjectInvalidRelation[]; +} + +export interface ScrollCountBodyHTTP { + typesToInclude: string[]; + // TODO: Fix. this API allows writing an arbitrary query that is passed straight to our persistence layer, thus leaking SO attributes to the public... + searchString?: string; + references?: Array<{ type: string; id: string }>; +} + +export interface DeleteObjectBodyHTTP { + id: string; + type: string; +} + +export interface DeleteObjectResponseHTTP { + id: string; +} + +/** + * In this case "string" is a direct mapping from "typesToInlcude" in {@link ScrollCountBodyHTTP['typesToInclude']']} + */ +export type ScrollCountResponseHTTP = Record; diff --git a/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts b/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts index 30a02f8fa42a..3f98f1fc3955 100644 --- a/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/bulk_delete_objects.ts @@ -6,24 +6,14 @@ * Side Public License, v 1. */ -import { HttpStart } from '@kbn/core/public'; -import { SavedObjectError, SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common'; - -interface SavedObjectDeleteStatus { - id: string; - success: boolean; - type: string; - error?: SavedObjectError; -} +import type { HttpStart } from '@kbn/core/public'; +import type { v1 } from '../../common'; export function bulkDeleteObjects( http: HttpStart, - objects: SavedObjectTypeIdTuple[] -): Promise { - return http.post( - '/internal/kibana/management/saved_objects/_bulk_delete', - { - body: JSON.stringify(objects), - } - ); + objects: v1.BulkDeleteBodyHTTP +): Promise { + return http.post('/internal/kibana/management/saved_objects/_bulk_delete', { + body: JSON.stringify(objects), + }); } diff --git a/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts b/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts index 370939d62e1d..61d4ca8bb934 100644 --- a/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/bulk_get_objects.ts @@ -6,15 +6,14 @@ * Side Public License, v 1. */ -import { HttpStart } from '@kbn/core/public'; -import { SavedObjectWithMetadata } from '../types'; +import type { HttpStart } from '@kbn/core/public'; +import type { v1 } from '../../common'; export async function bulkGetObjects( http: HttpStart, - objects: Array<{ type: string; id: string }> -): Promise { - return await http.post( - `/api/kibana/management/saved_objects/_bulk_get`, - { body: JSON.stringify(objects) } - ); + objects: v1.BulkGetBodyHTTP +): Promise { + return await http.post(`/api/kibana/management/saved_objects/_bulk_get`, { + body: JSON.stringify(objects), + }); } diff --git a/src/plugins/saved_objects_management/public/lib/case_conversion.test.ts b/src/plugins/saved_objects_management/public/lib/case_conversion.test.ts deleted file mode 100644 index 111a62a1c5b9..000000000000 --- a/src/plugins/saved_objects_management/public/lib/case_conversion.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { keysToCamelCaseShallow } from './case_conversion'; - -describe('keysToCamelCaseShallow', () => { - test("should convert all of an object's keys to camel case", () => { - const data = { - camelCase: 'camelCase', - 'kebab-case': 'kebabCase', - snake_case: 'snakeCase', - }; - - const result = keysToCamelCaseShallow(data); - - expect(result.camelCase).toBe('camelCase'); - expect(result.kebabCase).toBe('kebabCase'); - expect(result.snakeCase).toBe('snakeCase'); - }); -}); diff --git a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts index eb14f96c700e..161d9034bcb7 100644 --- a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts +++ b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; +import type { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; export async function fetchExportByTypeAndSearch({ http, diff --git a/src/plugins/saved_objects_management/public/lib/find_objects.ts b/src/plugins/saved_objects_management/public/lib/find_objects.ts index aff66e0a08cc..658bb64800a3 100644 --- a/src/plugins/saved_objects_management/public/lib/find_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/find_objects.ts @@ -6,32 +6,17 @@ * Side Public License, v 1. */ -import { HttpStart, SavedObjectsFindOptions } from '@kbn/core/public'; -import { keysToCamelCaseShallow } from './case_conversion'; -import { SavedObjectWithMetadata } from '../types'; - -interface SavedObjectsFindResponse { - total: number; - page: number; - perPage: number; - savedObjects: SavedObjectWithMetadata[]; -} +import { HttpStart } from '@kbn/core/public'; +import type { v1 } from '../../common'; export async function findObjects( http: HttpStart, - findOptions: SavedObjectsFindOptions -): Promise { - const response = await http.get>( - '/api/kibana/management/saved_objects/_find', - { - query: { - ...findOptions, - hasReference: findOptions.hasReference - ? JSON.stringify(findOptions.hasReference) - : undefined, - } as Record, - } - ); - - return keysToCamelCaseShallow(response) as SavedObjectsFindResponse; + findOptions: v1.FindQueryHTTP +): Promise { + return http.get('/api/kibana/management/saved_objects/_find', { + query: { + ...findOptions, + hasReference: findOptions.hasReference ? JSON.stringify(findOptions.hasReference) : undefined, + } as Record, + }); } diff --git a/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts b/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts index 7e68dbc6be37..3bf9c0f44d2c 100644 --- a/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts +++ b/src/plugins/saved_objects_management/public/lib/get_allowed_types.ts @@ -7,14 +7,12 @@ */ import type { HttpStart } from '@kbn/core/public'; -import type { SavedObjectManagementTypeInfo } from '../../common/types'; +import type { v1 } from '../../common'; -interface GetAllowedTypesResponse { - types: SavedObjectManagementTypeInfo[]; -} - -export async function getAllowedTypes(http: HttpStart): Promise { - const response = await http.get( +export async function getAllowedTypes( + http: HttpStart +): Promise { + const response = await http.get( '/api/kibana/management/saved_objects/_allowed_types' ); return response.types; diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts index c52ce26e96a3..c3e001588802 100644 --- a/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SavedObjectGetRelationshipsResponse } from '../types'; +import { v1 } from '../../common'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { getRelationships } from './get_relationships'; @@ -23,7 +23,7 @@ describe('getRelationships', () => { }); it('should handle successful responses', async () => { - const serverResponse: SavedObjectGetRelationshipsResponse = { + const serverResponse: v1.RelationshipsResponseHTTP = { relations: [], invalidRelations: [], }; diff --git a/src/plugins/saved_objects_management/public/lib/get_relationships.ts b/src/plugins/saved_objects_management/public/lib/get_relationships.ts index f0431144573d..7647da05029d 100644 --- a/src/plugins/saved_objects_management/public/lib/get_relationships.ts +++ b/src/plugins/saved_objects_management/public/lib/get_relationships.ts @@ -6,21 +6,21 @@ * Side Public License, v 1. */ -import { HttpStart } from '@kbn/core/public'; +import type { HttpStart } from '@kbn/core/public'; import { get } from 'lodash'; -import { SavedObjectGetRelationshipsResponse } from '../types'; +import type { v1 } from '../../common'; export async function getRelationships( http: HttpStart, type: string, id: string, savedObjectTypes: string[] -): Promise { +): Promise { const url = `/api/kibana/management/saved_objects/relationships/${encodeURIComponent( type )}/${encodeURIComponent(id)}`; try { - return await http.get(url, { + return await http.get(url, { query: { savedObjectTypes, }, diff --git a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts index 6d31d7085606..23d2818257be 100644 --- a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts +++ b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; +import type { HttpStart, SavedObjectsFindOptionsReference } from '@kbn/core/public'; +import type { v1 } from '../../common'; export async function getSavedObjectCounts({ http, @@ -18,9 +19,8 @@ export async function getSavedObjectCounts({ typesToInclude: string[]; searchString?: string; references?: SavedObjectsFindOptionsReference[]; -}): Promise> { - return await http.post>( - `/api/kibana/management/saved_objects/scroll/counts`, - { body: JSON.stringify({ typesToInclude, searchString, references }) } - ); +}): Promise { + return await http.post(`/api/kibana/management/saved_objects/scroll/counts`, { + body: JSON.stringify({ typesToInclude, searchString, references }), + }); } diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts index 6e354d994220..6b54e7cc617f 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, SavedObjectsImportUnknownError, diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index 4f2624c73ed1..480c10a345cd 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { SavedObjectsImportResponse, SavedObjectsImportConflictError, SavedObjectsImportAmbiguousConflictError, diff --git a/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts index 9e9062709674..7558af02cd2a 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { HttpStart, SavedObjectsImportConflictError, SavedObjectsImportRetry, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index bb097f64c443..825e66197913 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -28,17 +28,17 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { IBasePath } from '@kbn/core/public'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; +import type { v1 } from '../../../../common'; import { SavedObjectWithMetadata, SavedObjectRelationKind, SavedObjectRelation, SavedObjectInvalidRelation, - SavedObjectGetRelationshipsResponse, } from '../../../types'; export interface RelationshipsProps { basePath: IBasePath; - getRelationships: (type: string, id: string) => Promise; + getRelationships: (type: string, id: string) => Promise; savedObject: SavedObjectWithMetadata; close: () => void; goInspectObject: (obj: SavedObjectWithMetadata) => void; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 69ced5010c56..c4ec166799f3 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -121,6 +121,7 @@ describe('SavedObjectsTable', () => { }; http.post.mockResolvedValue([]); + http.delete.mockResolvedValue({ id: 'test' }); getSavedObjectCountsMock.mockReturnValue({ 'index-pattern': 0, @@ -147,7 +148,7 @@ describe('SavedObjectsTable', () => { findObjectsMock.mockImplementation(() => ({ total: 4, - savedObjects: [ + saved_objects: [ { id: '1', type: 'index-pattern', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 1762ccafe698..9f6bf01f46ca 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -34,6 +34,7 @@ import { SavedObjectsExportResultDetails, getTagFindReferences, } from '../../lib'; + import { SavedObjectWithMetadata } from '../../types'; import { SavedObjectsManagementActionServiceStart, @@ -257,7 +258,7 @@ export class SavedObjectsTable extends Component Promise; + ) => Promise; getSavedObjectLabel: typeof getSavedObjectLabel; getDefaultTitle: typeof getDefaultTitle; parseQuery: typeof parseQuery; diff --git a/src/plugins/saved_objects_management/public/types.ts b/src/plugins/saved_objects_management/public/types.ts index 91e6e58e3dd0..fbe8a0cac894 100644 --- a/src/plugins/saved_objects_management/public/types.ts +++ b/src/plugins/saved_objects_management/public/types.ts @@ -12,6 +12,5 @@ export type { SavedObjectRelationKind, SavedObjectRelation, SavedObjectInvalidRelation, - SavedObjectGetRelationshipsResponse, SavedObjectManagementTypeInfo, } from '../common'; diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.ts index 329d985b0b26..00fae0b24ab4 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.ts @@ -9,11 +9,8 @@ import { SavedObjectsClientContract } from '@kbn/core/server'; import { injectMetaAttributes } from './inject_meta_attributes'; import { ISavedObjectsManagement } from '../services'; -import { - SavedObjectInvalidRelation, - SavedObjectWithMetadata, - SavedObjectGetRelationshipsResponse, -} from '../types'; +import { v1 } from '../../common'; +import { SavedObjectInvalidRelation, SavedObjectWithMetadata } from '../types'; export async function findRelationships({ type, @@ -29,7 +26,7 @@ export async function findRelationships({ client: SavedObjectsClientContract; referenceTypes: string[]; savedObjectsManagement: ISavedObjectsManagement; -}): Promise { +}): Promise { const { references = [] } = await client.get(type, id); // Use a map to avoid duplicates, it does happen but have a different "name" in the reference diff --git a/src/plugins/saved_objects_management/server/lib/index.ts b/src/plugins/saved_objects_management/server/lib/index.ts index cfb5a124bea5..9ee181246075 100644 --- a/src/plugins/saved_objects_management/server/lib/index.ts +++ b/src/plugins/saved_objects_management/server/lib/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export { toSavedObjectWithMeta } from './to_saved_object_with_meta'; export { injectMetaAttributes } from './inject_meta_attributes'; export { findAll } from './find_all'; export { findRelationships } from './find_relationships'; diff --git a/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts b/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts new file mode 100644 index 000000000000..03a900b0ddc9 --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/to_saved_object_with_meta.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SavedObject } from '@kbn/core/server'; +import { SavedObjectWithMetadata } from '../../common/types/v1'; + +export function toSavedObjectWithMeta(so: SavedObject): SavedObjectWithMetadata { + return { + id: so.id, + type: so.type, + namespaces: so.namespaces, + references: so.references, + updated_at: so.updated_at, + attributes: so.attributes, + created_at: so.created_at, + error: so.error, + meta: {}, + }; +} diff --git a/src/plugins/saved_objects_management/server/routes/bulk_delete.ts b/src/plugins/saved_objects_management/server/routes/bulk_delete.ts index e3f4db044ef2..1776a73a7504 100644 --- a/src/plugins/saved_objects_management/server/routes/bulk_delete.ts +++ b/src/plugins/saved_objects_management/server/routes/bulk_delete.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; +import type { v1 } from '../../common'; export const registerBulkDeleteRoute = (router: IRouter) => { router.post( @@ -29,7 +30,8 @@ export const registerBulkDeleteRoute = (router: IRouter) => { const client = getClient(); const response = await client.bulkDelete(objects, { force: true }); - return res.ok({ body: response.statuses }); + const body: v1.BulkDeleteResponseHTTP = response.statuses; + return res.ok({ body }); }) ); }; diff --git a/src/plugins/saved_objects_management/server/routes/bulk_get.ts b/src/plugins/saved_objects_management/server/routes/bulk_get.ts index 9e31b1c24b0b..c93645cef650 100644 --- a/src/plugins/saved_objects_management/server/routes/bulk_get.ts +++ b/src/plugins/saved_objects_management/server/routes/bulk_get.ts @@ -7,9 +7,10 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; -import { injectMetaAttributes } from '../lib'; -import { ISavedObjectsManagement } from '../services'; +import type { IRouter } from '@kbn/core/server'; +import { injectMetaAttributes, toSavedObjectWithMeta } from '../lib'; +import type { v1 } from '../../common'; +import type { ISavedObjectsManagement } from '../services'; export const registerBulkGetRoute = ( router: IRouter, @@ -39,14 +40,16 @@ export const registerBulkGetRoute = ( const client = getClient({ includedHiddenTypes }); const response = await client.bulkGet(objects); - const enhancedObjects = response.saved_objects.map((obj) => { - if (!obj.error) { + + const body: v1.BulkGetResponseHTTP = response.saved_objects.map((obj) => { + const so = toSavedObjectWithMeta(obj); + if (!so.error) { return injectMetaAttributes(obj, managementService); } - return obj; + return so; }); - return res.ok({ body: enhancedObjects }); + return res.ok({ body }); }) ); }; diff --git a/src/plugins/saved_objects_management/server/routes/find.ts b/src/plugins/saved_objects_management/server/routes/find.ts index 7b9e8b7c0ddc..bf5810c4beb9 100644 --- a/src/plugins/saved_objects_management/server/routes/find.ts +++ b/src/plugins/saved_objects_management/server/routes/find.ts @@ -7,9 +7,11 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; -import { injectMetaAttributes } from '../lib'; -import { ISavedObjectsManagement } from '../services'; +import type { IRouter } from '@kbn/core/server'; + +import type { v1 } from '../../common'; +import { injectMetaAttributes, toSavedObjectWithMeta } from '../lib'; +import type { ISavedObjectsManagement } from '../services'; export const registerFindRoute = ( router: IRouter, @@ -77,22 +79,23 @@ export const registerFindRoute = ( searchFields: [...searchFields], }); - const enhancedSavedObjects = findResponse.saved_objects - .map((so) => injectMetaAttributes(so, managementService)) - .map((obj) => { - const result = { ...obj, attributes: {} as Record }; + const savedObjects = findResponse.saved_objects.map(toSavedObjectWithMeta); + + const response: v1.FindResponseHTTP = { + saved_objects: savedObjects.map((so) => { + const obj = injectMetaAttributes(so, managementService); + const result = { ...obj, attributes: {} as Record }; for (const field of includedFields) { - result.attributes[field] = obj.attributes[field]; + result.attributes[field] = (obj.attributes as Record)[field]; } return result; - }); + }), + total: findResponse.total, + per_page: findResponse.per_page, + page: findResponse.page, + }; - return res.ok({ - body: { - ...findResponse, - saved_objects: enhancedSavedObjects, - }, - }); + return res.ok({ body: response }); }) ); }; diff --git a/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts index cdd6dc215d69..3a6b0e5809d1 100644 --- a/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts +++ b/src/plugins/saved_objects_management/server/routes/get_allowed_types.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { IRouter, SavedObjectsType } from '@kbn/core/server'; -import { SavedObjectManagementTypeInfo } from '../../common'; +import type { IRouter, SavedObjectsType } from '@kbn/core/server'; +import type { SavedObjectManagementTypeInfo } from '../../common'; const convertType = (sot: SavedObjectsType): SavedObjectManagementTypeInfo => { return { diff --git a/src/plugins/saved_objects_management/server/routes/relationships.ts b/src/plugins/saved_objects_management/server/routes/relationships.ts index 8900987a645f..91cbbbabb6de 100644 --- a/src/plugins/saved_objects_management/server/routes/relationships.ts +++ b/src/plugins/saved_objects_management/server/routes/relationships.ts @@ -7,10 +7,11 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; +import type { IRouter } from '@kbn/core/server'; import { chain } from 'lodash'; import { findRelationships } from '../lib'; -import { ISavedObjectsManagement } from '../services'; +import type { ISavedObjectsManagement } from '../services'; +import type { v1 } from '../../common'; export const registerRelationshipsRoute = ( router: IRouter, @@ -48,7 +49,7 @@ export const registerRelationshipsRoute = ( const client = getClient({ includedHiddenTypes }); - const findRelationsResponse = await findRelationships({ + const findRelationsResponse: v1.RelationshipsResponseHTTP = await findRelationships({ type, id, client, diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index 26dd1d57b4cf..210bb3b27c67 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -7,8 +7,9 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, SavedObjectsCreatePointInTimeFinderOptions } from '@kbn/core/server'; +import type { IRouter, SavedObjectsCreatePointInTimeFinderOptions } from '@kbn/core/server'; import { chain } from 'lodash'; +import type { v1 } from '../../common'; import { findAll } from '../lib'; export const registerScrollForCountRoute = (router: IRouter) => { @@ -70,8 +71,10 @@ export const registerScrollForCountRoute = (router: IRouter) => { } } + const body: v1.ScrollCountResponseHTTP = counts; + return res.ok({ - body: counts, + body, }); }) ); diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts index 93f6f3d09547..00cef5f0dbea 100644 --- a/src/plugins/saved_objects_management/server/types.ts +++ b/src/plugins/saved_objects_management/server/types.ts @@ -18,5 +18,4 @@ export type { SavedObjectRelationKind, SavedObjectRelation, SavedObjectInvalidRelation, - SavedObjectGetRelationshipsResponse, } from '../common'; diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index 98dcb8c30317..0ed7eee5b203 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -22,7 +22,6 @@ "@kbn/i18n-react", "@kbn/test-jest-helpers", "@kbn/core-saved-objects-api-server", - "@kbn/core-saved-objects-common", "@kbn/monaco", "@kbn/config-schema", "@kbn/core-custom-branding-browser-mocks", diff --git a/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts index 9e154ddde153..c4cbd575dc06 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/visible_in_management.ts @@ -9,7 +9,7 @@ import { join } from 'path'; import expect from '@kbn/expect'; import type { Response } from 'supertest'; -import { SavedObject } from '@kbn/core/types'; +import { SavedObject } from '@kbn/core/server'; import type { SavedObjectManagementTypeInfo } from '@kbn/saved-objects-management-plugin/common/types'; import type { PluginFunctionalProviderContext } from '../../services'; From 1418d753eacd0095cca4f08af5c5b12cc9af0817 Mon Sep 17 00:00:00 2001 From: Jeramy Soucy Date: Thu, 2 Feb 2023 11:11:35 -0500 Subject: [PATCH 21/35] [Docs] Adds authentication providers sync to load balancing documentation (#149961) Closes #113928 ## Summary - Adds 'xpack.security.authc.providers' to the list of settings that must be the same across all Kibana instances behind a load balancer. - Adds a warning block explaining why the authentication providers need to match, and an additional configuration case where this applies (Kibana instances that are backed by the same ES instance and share the same kibana.index). --- docs/user/production-considerations/production.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 84727e536cfe..92cb77cc401f 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -51,11 +51,18 @@ Settings that must be the same: [source,js] -------- xpack.security.encryptionKey //decrypting session information +xpack.security.authc.* // authentication configuration +xpack.security.session.* // session configuration xpack.reporting.encryptionKey //decrypting reports xpack.encryptedSavedObjects.encryptionKey // decrypting saved objects xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys // saved objects encryption key rotation, if any -------- +[WARNING] +==== +If the authentication configuration does not match, sessions from unrecognized providers in each {kib} instance will be deleted during that instance's regular session cleanup. Similarly, inconsistencies in session configuration can also lead to undesired session logouts. This also applies to any {kib} instances that are backed by the same {es} instance and share the same kibana.index, even if they are not behind the same load balancer. +==== + Separate configuration files can be used from the command line by using the `-c` flag: [source,js] -------- From 927091dfea916fbed0a8d1f6e6481294dd057f70 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Thu, 2 Feb 2023 17:13:01 +0100 Subject: [PATCH 22/35] Fixed back navigation (#150042) --- .../view/artifacts/layout/policy_artifacts_layout.tsx | 1 + .../public/management/pages/policy/view/policy_hooks.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx index baecdb8d03aa..666bb5534d3e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx @@ -59,6 +59,7 @@ export const PolicyArtifactsLayout = React.memo( () => getExceptionsListApiClient(), [getExceptionsListApiClient] ); + const { getAppUrl } = useAppUrl(); const navigateCallback = usePolicyDetailsArtifactsNavigateCallback( exceptionsListApiClient.listId diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts index 001e3c5727a5..26424205db01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_hooks.ts @@ -6,7 +6,7 @@ */ import { useCallback, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { ENDPOINT_BLOCKLISTS_LIST_ID, @@ -50,7 +50,7 @@ export function usePolicyDetailsArtifactsNavigateCallback(listId: string) { const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); const history = useHistory(); const policyId = usePolicyDetailsSelector(policyIdFromParams); - + const { state } = useLocation(); const getPath = useCallback( (args: Partial) => { if (listId === ENDPOINT_TRUSTED_APPS_LIST_ID) { @@ -79,8 +79,8 @@ export function usePolicyDetailsArtifactsNavigateCallback(listId: string) { ); return useCallback( - (args: Partial) => history.push(getPath(args)), - [getPath, history] + (args: Partial) => history.push(getPath(args), state), + [getPath, history, state] ); } From ff39dca4a866f839e34ba651068c8016561422e2 Mon Sep 17 00:00:00 2001 From: Aleksandr Maus Date: Thu, 2 Feb 2023 11:17:17 -0500 Subject: [PATCH 23/35] Osquery: Update exported fields reference for osquery 5.5.1 (#143754) --- .../exported-fields-reference.asciidoc | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/docs/osquery/exported-fields-reference.asciidoc b/docs/osquery/exported-fields-reference.asciidoc index c0405f3e6568..c27b6e67a406 100644 --- a/docs/osquery/exported-fields-reference.asciidoc +++ b/docs/osquery/exported-fields-reference.asciidoc @@ -80,6 +80,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _systemd_units.active_state_ - The high-level unit activation state, i.e. generalization of SUB +*activity* - keyword, number.long + +* _unified_log.activity_ - the activity ID associate with the entry. + *actual* - keyword, number.long * _fan_speed_sensors.actual_ - Actual speed @@ -114,7 +118,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *algorithm* - keyword, text.text -* _authorized_keys.algorithm_ - algorithm of key +* _authorized_keys.algorithm_ - Key type *alias* - keyword, text.text @@ -621,6 +625,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _ntfs_journal_events.category_ - The category that the event originated from * _power_sensors.category_ - The sensor category: currents, voltage, wattage * _system_extensions.category_ - System extension category +* _unified_log.category_ - The category of the os_log_t used * _yara_events.category_ - The category of the file *cdhash* - keyword, text.text @@ -645,6 +650,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _docker_containers.cgroup_namespace_ - cgroup namespace * _process_namespaces.cgroup_namespace_ - cgroup namespace inode +*cgroup_path* - keyword, text.text + +* _processes.cgroup_path_ - The full hierarchical path of the process's control group + *chain* - keyword, text.text * _iptables.chain_ - Size of module content. @@ -836,9 +845,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *comment* - keyword, text.text * _authorizations.comment_ - Label top-level key +* _authorized_keys.comment_ - Optional comment * _docker_image_history.comment_ - Instruction comment * _etc_protocols.comment_ - Comment with protocol description -* _etc_services.comment_ - Optional comment for a service. +* _etc_services.comment_ - Optional comment for a service * _groups.comment_ - Remarks or comments associated with the group * _keychain_items.comment_ - Optional keychain comment @@ -1937,6 +1947,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _alf.firewall_unload_ - 1 If firewall unloading enabled else 0 +*firmware_type* - keyword, text.text + +* _platform_info.firmware_type_ - The type of firmware (Uefi, Bios, Unknown). + *firmware_version* - keyword, text.text * _ibridge_info.firmware_version_ - The build version of the firmware @@ -2236,7 +2250,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *hostname* - keyword, text.text -* _curl_certificate.hostname_ - Hostname (domain[:port]) to CURL +* _curl_certificate.hostname_ - Hostname to CURL (domain[:port], for example, osquery.io) * _system_info.hostname_ - Network hostname including domain * _ycloud_instance_metadata.hostname_ - Hostname of the VM @@ -2683,7 +2697,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *key* - keyword, text.text -* _authorized_keys.key_ - parsed authorized keys line +* _authorized_keys.key_ - Key encoded as base64 * _azure_instance_tags.key_ - The tag key * _chrome_extensions.key_ - The extension key, from the manifest file * _docker_container_envs.key_ - Environment variable name @@ -2857,9 +2871,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _docker_image_layers.layer_order_ - Layer Order (1 = base layer) -*level* - keyword, number.long +*level* - keyword * _asl.level_ - Log level number. See levels in asl.h. +* _unified_log.level_ - the severity level of the entry * _windows_eventlog.level_ - Severity level associated with the event * _windows_events.level_ - The severity level associated with the event @@ -3093,6 +3108,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _pipes.max_instances_ - The maximum number of instances creatable for this pipe +*max_rows* - keyword, number.long + +* _unified_log.max_rows_ - The max number of rows returned (defaults to 100). + *max_speed* - keyword, number.long * _memory_devices.max_speed_ - Max speed of memory device in megatransfers per second (MT/s) @@ -3221,6 +3240,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _lxd_cluster_members.message_ - Message from the node (Online/Offline) * _selinux_events.message_ - Message * _syslog_events.message_ - The syslog message +* _unified_log.message_ - Composed message * _user_events.message_ - Message from the event *metadata_endpoint* - keyword, text.text @@ -3699,8 +3719,9 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _chrome_extensions.optional_permissions_json_ - The JSON-encoded permissions optionally required by the extensions -*options* - keyword +*options* - keyword, text.text +* _authorized_keys.options_ - Optional list of login options * _dns_resolvers.options_ - Resolver options * _nfs_shares.options_ - Options string set on the export share @@ -4129,9 +4150,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _processes.pid_ - Process (or thread) ID * _running_apps.pid_ - The pid of the application * _seccomp_events.pid_ - Process ID -* _services.pid_ - the Process ID of the service +* _services.pid_ - The Process ID of the service * _shared_memory.pid_ - Process ID to last use the segment * _socket_events.pid_ - Process (or thread) ID +* _unified_log.pid_ - The pid of the process that made the entry * _user_events.pid_ - Process (or thread) ID * _windows_crashes.pid_ - Process ID of the crashed process * _windows_eventlog.pid_ - Process ID which emitted the event record @@ -4305,6 +4327,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *process* - keyword, text.text * _alf_explicit_auths.process_ - Process name explicitly allowed +* _unified_log.process_ - The name of the process that made the entry *process_being_tapped* - keyword, number.long @@ -4852,6 +4875,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *sender* - keyword, text.text * _asl.sender_ - Sender's identification string. Default is process name. +* _unified_log.sender_ - The name of the binary image that made the entry *sensor_backend_server* - keyword, text.text @@ -5311,6 +5335,10 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _kva_speculative_info.stibp_support_enabled_ - Windows uses STIBP. +*storage* - keyword, number.long + +* _unified_log.storage_ - The storage category for the entry. + *storage_driver* - keyword, text.text * _docker_info.storage_driver_ - Storage driver @@ -5388,6 +5416,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *subsystem* - keyword, text.text * _system_controls.subsystem_ - Subsystem ID, control type +* _unified_log.subsystem_ - The subsystem of the os_log_t used *subsystem_model* - keyword, text.text @@ -5556,6 +5585,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _bpf_process_events.tid_ - Thread ID * _bpf_socket_events.tid_ - Thread ID +* _unified_log.tid_ - The tid of the thread that made the entry * _windows_crashes.tid_ - Thread ID of the crashed thread * _windows_eventlog.tid_ - Thread ID which emitted the event record @@ -5607,6 +5637,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq *timestamp* - keyword, text.text * _time.timestamp_ - Current timestamp (log format) in UTC +* _unified_log.timestamp_ - Unix timestamp associated with the entry * _windows_eventlog.timestamp_ - Timestamp to selectively filter the events *timestamp_ms* - keyword, number.long @@ -5697,7 +5728,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _ntfs_acl_permissions.type_ - Type of access mode for the access control entry. * _nvram.type_ - Data type (CFData, CFString, etc) * _osquery_events.type_ - Either publisher or subscriber -* _osquery_extensions.type_ - SDK extension type: extension or module +* _osquery_extensions.type_ - SDK extension type: core, extension, or module * _osquery_flags.type_ - Flag type * _process_open_pipes.type_ - Pipe Type: named vs unnamed/anonymous * _registry.type_ - Type of the registry value, or 'subkey' if item is a subkey @@ -5742,7 +5773,7 @@ For more information about osquery tables, see the https://osquery.io/schema[osq * _known_hosts.uid_ - The local user that owns the known_hosts file * _launchd_overrides.uid_ - User ID applied to the override, 0 applies to all * _package_bom.uid_ - Expected user of file or directory -* _password_policy.uid_ - User ID for the policy if available +* _password_policy.uid_ - User ID for the policy, -1 for policies that are global * _process_events.uid_ - User ID at process start * _process_file_events.uid_ - The uid of the process performing the action * _processes.uid_ - Unsigned user ID From d854a9a35b903114d5ba0978e48a082fb0e3b5b3 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 2 Feb 2023 09:40:39 -0700 Subject: [PATCH 24/35] [ML] Data Frame Analytics: adds functional test for scatterplot chart link to custom visualization (#150103) ## Summary Follow up to https://github.com/elastic/kibana/pull/149647 Link to flaky test runner: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1856 This PR adds a functional test for the scatterplot matrix link in the DFA results view to custom visualizations UI. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../scatterplot_matrix/scatterplot_matrix.tsx | 2 +- .../results_view_content.ts | 4 ++++ .../ml/data_frame_analytics_results.ts | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 6e718e0f0ccd..9a10dc2b782c 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -532,7 +532,7 @@ export const ScatterplotMatrix: FC = ({ openInNewTab: false, }); }} - data-test-subj="mlSplomoExploreInCustomVisualizationLink" + data-test-subj="mlSplomExploreInCustomVisualizationLink" > { + await ml.dataFrameAnalyticsResults.assertOpensExploreInCustomVisualization(); + }); }); } }); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index 65531647d00e..a4378636673d 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -20,6 +20,7 @@ export function MachineLearningDataFrameAnalyticsResultsProvider( const headerPage = getPageObject('header'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const find = getService('find'); return { async assertRegressionEvaluatePanelElementsExists() { @@ -74,6 +75,26 @@ export function MachineLearningDataFrameAnalyticsResultsProvider( ); }, + async getViewContainer() { + return find.byCssSelector('div.vgaVis__view'); + }, + + async assertOpensExploreInCustomVisualization() { + await testSubjects.existOrFail('mlSplomExploreInCustomVisualizationLink', { + timeout: 5000, + }); + await testSubjects.click('mlSplomExploreInCustomVisualizationLink'); + await testSubjects.existOrFail('visualizationLoader'); + + const view = await this.getViewContainer(); + expect(view).to.be.ok(); + const size = await view.getSize(); + expect(size).to.have.property('width'); + expect(size).to.have.property('height'); + expect(size.width).to.be.above(0); + expect(size.height).to.be.above(0); + }, + async enableResultsTablePreviewHistogramCharts(expectedButtonState: boolean) { await retry.tryForTime(5000, async () => { const actualState = From 801377f682ca39a2f171372e21e4e2a97f57fffd Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 2 Feb 2023 09:41:04 -0700 Subject: [PATCH 25/35] [ML] Data Frame Analytics maps view: Fix update of map when selecting results index node (#149993) ## Summary Fixes https://github.com/elastic/kibana/issues/147775 Ensures node details persist between renders so they don't get overwritten when refetching from the original source node, causing the whole map to re-render https://user-images.githubusercontent.com/6446462/215898574-d6410492-3ca6-4879-b103-a774c3e4f428.mp4 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../job_map/use_fetch_analytics_map_data.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts index a1660f126bbb..846e27288491 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/use_fetch_analytics_map_data.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { i18n } from '@kbn/i18n'; +import { asyncForEach } from '@kbn/std'; import { uniqWith, isEqual } from 'lodash'; import cytoscape from 'cytoscape'; import { ml } from '../../../services/ml_api_service'; @@ -23,11 +24,11 @@ interface GetDataObjectParameter { export const useFetchAnalyticsMapData = () => { const [isLoading, setIsLoading] = useState(false); const [elements, setElements] = useState([]); - const [nodeDetails, setNodeDetails] = useState>({}); const [error, setError] = useState(); const [message, setMessage] = useState(); // Keeps track of which nodes have been used as root so we can refetch related nodes on refresh const [usedAsRoot, setUsedAsRoot] = useState>({}); + const nodeDetails = useRef>({}); const fetchAndSetElements = async (idToUse: string, treatAsRoot: boolean, type?: string) => { setIsLoading(true); @@ -57,11 +58,11 @@ export const useFetchAnalyticsMapData = () => { if (nodeElements?.length > 0) { if (treatAsRoot === false) { setElements(nodeElements); - setNodeDetails(details); + nodeDetails.current = details; } else { const uniqueElements = uniqWith([...nodeElements, ...elements], isEqual); setElements(uniqueElements); - setNodeDetails({ ...details, ...nodeDetails }); + nodeDetails.current = { ...details, ...nodeDetails.current }; } } setIsLoading(false); @@ -88,11 +89,9 @@ export const useFetchAnalyticsMapData = () => { // If related nodes had been fetched from any node then refetch if (Object.keys(usedAsRoot).length) { - for (const nodeId in usedAsRoot) { - if (usedAsRoot.hasOwnProperty(nodeId)) { - await fetchAndSetElements(nodeId, true, usedAsRoot[nodeId]); - } - } + await asyncForEach(Object.keys(usedAsRoot), async (nodeId) => { + await fetchAndSetElements(nodeId, true, usedAsRoot[nodeId]); + }); } }; @@ -102,7 +101,7 @@ export const useFetchAnalyticsMapData = () => { fetchAndSetElementsWrapper, isLoading, message, - nodeDetails, + nodeDetails: nodeDetails.current, setElements, setError, }; From e9b3bbb8f8f004a9171bc34ba4054df4ea0bd1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Thu, 2 Feb 2023 18:04:46 +0100 Subject: [PATCH 26/35] Use all monitors when checking loading state (#150148) ## Summary Closes #149978 The error and alerts graphs in the monitor overview page only consider the enabled monitors to handle their internal "loading" state. This made the graphs show a permanent "loading" state if no monitors were enabled, instead of showing zero values for those monitors. This PR uses all the monitor IDs, enabled or not, to determine if the monitors have been loaded. It still uses the enabled monitor IDs for the graphs data so the data shown is correct. --- .../monitors_page/overview/overview/overview_alerts.tsx | 2 +- .../overview/overview/overview_errors/overview_errors.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx index 0d2cbe7e3bfc..d6b3d642b177 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx @@ -33,7 +33,7 @@ export const OverviewAlerts = () => { const { status } = useSelector(selectOverviewStatus); - const loading = !status?.enabledIds || status?.enabledIds.length === 0; + const loading = !status?.allIds || status?.allIds.length === 0; return ( diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx index 28f0c9f9284b..b2f040011594 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors.tsx @@ -25,7 +25,7 @@ import { selectOverviewStatus } from '../../../../../state'; export function OverviewErrors() { const { status } = useSelector(selectOverviewStatus); - const loading = !status?.enabledIds || status?.enabledIds.length === 0; + const loading = !status?.allIds || status?.allIds.length === 0; const { from, to } = useAbsoluteDate({ from: 'now-6h', to: 'now' }); From b9488a099c1e23d33dd87639d3383dd5df3a58fd Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Thu, 2 Feb 2023 18:11:38 +0100 Subject: [PATCH 27/35] Terms query for Indicator Match rule (#144511) ## Terms query for Indicator Match rule TODO: [] need more unit/integrations tests, but ready for review The indicator match rule will use terms query when it is possible to search for matches for threat-first-search and for events-first-search. ## How the match query worked: Example for threat-first-search. If we have matching conditions like: `host.ip ==== indicator.host.ip` or (`source.name === indicator.source.name` AND `host.name === indicator.host.name`) It will generate queries like: ``` match: {host.ip: "1"}, or match: {host.ip: "2"} or match: {host.ip: "3"} or (match: {source.name: "1"} and match: {host.name: "1"}) or (match: {source.name: "2"} and match: {host.name: "2"}) or (match: {source.name: "3"} and match: {host.name: "3"}) ``` Each match will also have `_name` fields like: `${threatId}_${threatIndex}_${threatFields}_${sourceField}` So and because it's 1:1 relation between match and response, later at enrichment stage will be clear which threat matches which event. ## Terms query. We do fetch info about mapping for fields which use for match conditions of the IM rule. Terms query doesn't support all field types, this is why there is some allowed list which field types. Terms query not applied for AND conditions. For example: Fields types host.ip - `ip` user.name - `keyword` user.description - `text` indicator.host.ip_range - `ip_range` `host.ip === indicator.host.ip` or `host.ip_range === indicator.host.ip` or (`source.name === indicator.source.name` AND `host.name === indicator.host.name`) It will generate queries like: ``` terms: {host.ip: ["1","2","3"]}, or match: {host.ip_range: "1"} // terms query support range fields, but it will be difficult later to understand which threat match which event, because we can have more than 1 response for this condition or match: {host.ip_range: "2"} or (match: {source.name: "1"} and match: {host.name: "1"}) or (match: {source.name: "2"} and match: {host.name: "2"}) or (match: {source.name: "3"} and match: {host.name: "3"}) ``` For terms query, we don't know which response matches with events, this is why we do match it back in the code. ## Other changes Threat-first-search - will do one extra request to have all matched threats. For example: The threat index has 1.000.000 documents. IM rule gets the first batch of 9.000 threats and builds a query to the events index. It returns 100 events (max_signal = 100). Then it tries to enrich those 100 events with threat info. The problem is that the original implementation will enrich with the only threats from this 9.000 batch. And it will ignore other matches in 1.000.000 threats. This way we do one extra request in the end from potential alerts to threat index. # Tests performance In the best case, it can improve performance by around 3x times. [Base](https://github.com/elastic/kibana/pull/149113) Threat Indicators - 1.500.000 documents Source - 1.000.000 documents. 1 field for match condition 213484531-3ab68c61-c3f5-4e28-b2c4-c1e90a5b1775 This PR: Screenshot 2023-01-30 at 20 20 32 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../threat_mapping/build_threat_enrichment.ts | 56 ++-- .../build_threat_mapping_filter.test.ts | 64 ++++- .../build_threat_mapping_filter.ts | 103 ++++++-- .../threat_mapping/create_event_signal.ts | 19 +- .../threat_mapping/create_threat_signal.ts | 28 +- .../threat_mapping/create_threat_signals.ts | 44 ++-- .../enrich_signal_threat_matches.mock.ts | 1 + .../enrich_signal_threat_matches.test.ts | 248 ++++++++++++++++-- .../enrich_signal_threat_matches.ts | 102 ++++--- ...get_allowed_fields_for_terms_query.test.ts | 144 ++++++++++ .../get_allowed_fields_for_terms_query.ts | 76 ++++++ .../signals/threat_mapping/get_event_count.ts | 2 +- .../signals/threat_mapping/types.ts | 72 ++++- .../signals/threat_mapping/utils.test.ts | 155 ++++++++++- .../signals/threat_mapping/utils.ts | 89 ++++++- .../rule_execution_logic/threat_match.ts | 190 +++++++++++++- 16 files changed, 1232 insertions(+), 161 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts index 92ed93b8802b..32fe9b8dbe8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts @@ -6,10 +6,16 @@ */ import type { SignalsEnrichment } from '../types'; -import { enrichSignalThreatMatches } from './enrich_signal_threat_matches'; -import type { BuildThreatEnrichmentOptions, GetMatchedThreats } from './types'; -import { getThreatList } from './get_threat_list'; +import { + enrichSignalThreatMatches, + getSignalMatchesFromThreatList, +} from './enrich_signal_threat_matches'; +import type { BuildThreatEnrichmentOptions } from './types'; +import { buildThreatMappingFilter } from './build_threat_mapping_filter'; +import { getAllThreatListHits } from './get_threat_list'; +// we do want to make extra requests to the threat index to get enrichments from all threats +// previously we were enriched alerts only from `currentThreatList` but not all threats export const buildThreatEnrichment = ({ ruleExecutionLogger, services, @@ -22,39 +28,45 @@ export const buildThreatEnrichment = ({ reassignPitId, listClient, exceptionFilter, + threatMapping, + runtimeMappings, }: BuildThreatEnrichmentOptions): SignalsEnrichment => { - const getMatchedThreats: GetMatchedThreats = async (ids) => { - const matchedThreatsFilter = { - query: { - bool: { - filter: { - ids: { values: ids }, - }, - }, + return async (signals) => { + const threatFiltersFromEvents = buildThreatMappingFilter({ + threatMapping, + threatList: signals, + entryKey: 'field', + allowedFieldsForTermsQuery: { + source: {}, + threat: {}, }, - }; - const threatResponse = await getThreatList({ + }); + + const threatListHits = await getAllThreatListHits({ esClient: services.scopedClusterClient.asCurrentUser, - index: threatIndex, - language: threatLanguage, - perPage: undefined, + threatFilters: [...threatFilters, threatFiltersFromEvents], query: threatQuery, + language: threatLanguage, + index: threatIndex, ruleExecutionLogger, - searchAfter: undefined, - threatFilters: [...threatFilters, matchedThreatsFilter], threatListConfig: { _source: [`${threatIndicatorPath}.*`, 'threat.feed.*'], fields: undefined, }, pitId, reassignPitId, - runtimeMappings: undefined, + runtimeMappings, listClient, exceptionFilter, }); - return threatResponse.hits.hits; - }; + const signalMatches = getSignalMatchesFromThreatList(threatListHits); - return (signals) => enrichSignalThreatMatches(signals, getMatchedThreats, threatIndicatorPath); + return enrichSignalThreatMatches( + signals, + () => Promise.resolve(threatListHits), + threatIndicatorPath, + signalMatches + ); + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts index ea896dbb2130..a8076498327c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts @@ -332,7 +332,7 @@ describe('build_threat_mapping_filter', () => { const threatMapping = getThreatMappingMock(); const threatListItem = getThreatListSearchResponseMock().hits.hits[0]; const innerClause = createAndOrClauses({ threatMapping, threatListItem, entryKey: 'value' }); - expect(innerClause).toEqual(getThreatMappingFilterShouldMock()); + expect(innerClause).toEqual(getThreatMappingFilterShouldMock().bool.should); }); test('it should filter out data from entries that do not have mappings', () => { @@ -343,7 +343,7 @@ describe('build_threat_mapping_filter', () => { foo: 'bar', }; const innerClause = createAndOrClauses({ threatMapping, threatListItem, entryKey: 'value' }); - expect(innerClause).toEqual(getThreatMappingFilterShouldMock()); + expect(innerClause).toEqual(getThreatMappingFilterShouldMock().bool.should); }); test('it should return an empty boolean given an empty array', () => { @@ -353,7 +353,7 @@ describe('build_threat_mapping_filter', () => { threatListItem, entryKey: 'value', }); - expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } }); + expect(innerClause).toEqual([]); }); test('it should return an empty boolean clause given an empty object for a threat list item', () => { @@ -363,7 +363,7 @@ describe('build_threat_mapping_filter', () => { threatListItem: getThreatListItemMock({ _source: {}, fields: {} }), entryKey: 'value', }); - expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } }); + expect(innerClause).toEqual([]); }); }); @@ -446,6 +446,62 @@ describe('build_threat_mapping_filter', () => { }; expect(mapping).toEqual(expected); }); + + test('it should use terms query if allowedFieldsForTermsQuery provided', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock().hits.hits; + const mapping = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: 1024, + entryKey: 'value', + allowedFieldsForTermsQuery: { + source: { 'source.ip': true }, + threat: { 'source.ip': true }, + }, + }); + const mock = { ...getThreatMappingFilterShouldMock() }; + mock.bool.should.pop(); + + const expected: BooleanFilter = { + bool: { + should: [ + mock, + { + terms: { + _name: '__SEP____SEP__source.ip__SEP__source.ip__SEP__tq', + 'source.ip': ['127.0.0.1'], + }, + }, + ], + minimum_should_match: 1, + }, + }; + expect(mapping).toEqual(expected); + }); + + test('it should use match query if allowedFieldsForTermsQuery provided, but it is AND', () => { + const threatMapping = getThreatMappingMock(); + const threatList = getThreatListSearchResponseMock().hits.hits; + const mapping = buildEntriesMappingFilter({ + threatMapping, + threatList, + chunkSize: 1024, + entryKey: 'value', + allowedFieldsForTermsQuery: { + source: { 'host.name': true, 'host.ip': true }, + threat: { 'host.name': true, 'host.ip': true }, + }, + }); + + const expected: BooleanFilter = { + bool: { + should: [getThreatMappingFilterShouldMock()], + minimum_should_match: 1, + }, + }; + expect(mapping).toEqual(expected); + }); }); describe('splitShouldClauses', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts index bc59d490bc7f..0d2691760028 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts @@ -7,7 +7,11 @@ import get from 'lodash/fp/get'; import type { Filter } from '@kbn/es-query'; -import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { + ThreatMapping, + ThreatMappingEntries, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { BooleanFilter, BuildEntriesMappingFilterOptions, @@ -16,7 +20,9 @@ import type { CreateInnerAndClausesOptions, FilterThreatMappingOptions, SplitShouldClausesOptions, + TermQuery, } from './types'; +import { ThreatMatchQueryType } from './types'; import { encodeThreatMatchNamedQuery } from './utils'; export const MAX_CHUNK_SIZE = 1024; @@ -26,6 +32,7 @@ export const buildThreatMappingFilter = ({ threatList, chunkSize, entryKey = 'value', + allowedFieldsForTermsQuery, }: BuildThreatMappingFilterOptions): Filter => { const computedChunkSize = chunkSize ?? MAX_CHUNK_SIZE; if (computedChunkSize > 1024) { @@ -36,6 +43,7 @@ export const buildThreatMappingFilter = ({ threatList, chunkSize: computedChunkSize, entryKey, + allowedFieldsForTermsQuery, }); const filterChunk: Filter = { meta: { @@ -45,6 +53,7 @@ export const buildThreatMappingFilter = ({ }, query, }; + return filterChunk; }; @@ -91,6 +100,7 @@ export const createInnerAndClauses = ({ index: threatListItem._index, field: threatMappingEntry.field, value: threatMappingEntry.value, + queryType: ThreatMatchQueryType.match, }), }, }, @@ -108,8 +118,8 @@ export const createAndOrClauses = ({ threatMapping, threatListItem, entryKey, -}: CreateAndOrClausesOptions): BooleanFilter => { - const should = threatMapping.reduce((accum, threatMap) => { +}: CreateAndOrClausesOptions): QueryDslQueryContainer[] => { + const should = threatMapping.reduce((accum, threatMap) => { const innerAndClauses = createInnerAndClauses({ threatMappingEntries: threatMap.entries, threatListItem, @@ -123,7 +133,7 @@ export const createAndOrClauses = ({ } return accum; }, []); - return { bool: { should, minimum_should_match: 1 } }; + return should; }; export const buildEntriesMappingFilter = ({ @@ -131,26 +141,68 @@ export const buildEntriesMappingFilter = ({ threatList, chunkSize, entryKey, + allowedFieldsForTermsQuery, }: BuildEntriesMappingFilterOptions): BooleanFilter => { - const combinedShould = threatList.reduce((accum, threatListSearchItem) => { - const filteredEntries = filterThreatMapping({ - threatMapping, - threatListItem: threatListSearchItem, - entryKey, - }); - const queryWithAndOrClause = createAndOrClauses({ - threatMapping: filteredEntries, - threatListItem: threatListSearchItem, - entryKey, - }); - if (queryWithAndOrClause.bool.should.length !== 0) { - // These values can be 10k+ large, so using a push here for performance - accum.push(queryWithAndOrClause); - } - return accum; - }, []); - const should = splitShouldClauses({ should: combinedShould, chunkSize }); - return { bool: { should, minimum_should_match: 1 } }; + const allFieldAllowedForTermQuery = (entries: ThreatMappingEntries) => + entries.every( + (entry) => + allowedFieldsForTermsQuery?.source?.[entry.field] && + allowedFieldsForTermsQuery?.threat?.[entry.value] + ); + const combinedShould = threatMapping.reduce<{ + match: QueryDslQueryContainer[]; + term: TermQuery[]; + }>( + (acc, threatMap) => { + if (threatMap.entries.length > 1 || !allFieldAllowedForTermQuery(threatMap.entries)) { + threatList.forEach((threatListSearchItem) => { + const filteredEntries = filterThreatMapping({ + threatMapping: [threatMap], + threatListItem: threatListSearchItem, + entryKey, + }); + const queryWithAndOrClause = createAndOrClauses({ + threatMapping: filteredEntries, + threatListItem: threatListSearchItem, + entryKey, + }); + if (queryWithAndOrClause.length !== 0) { + // These values can be 10k+ large, so using a push here for performance + acc.match.push(...queryWithAndOrClause); + } + }); + } else { + const threatMappingEntry = threatMap.entries[0]; + const threats: string[] = threatList + .map((threatListItem) => get(threatMappingEntry[entryKey], threatListItem.fields)) + .filter((val) => val) + .map((val) => val[0]); + if (threats.length > 0) { + acc.term.push({ + terms: { + _name: encodeThreatMatchNamedQuery({ + field: threatMappingEntry.field, + value: threatMappingEntry.value, + queryType: ThreatMatchQueryType.term, + }), + [threatMappingEntry[entryKey === 'field' ? 'value' : 'field']]: threats, + }, + }); + } + } + return acc; + }, + { match: [], term: [] } + ); + + const matchShould = splitShouldClauses({ + should: + combinedShould.match.length > 0 + ? [{ bool: { should: combinedShould.match, minimum_should_match: 1 } }] + : [], + chunkSize, + }); + return { bool: { should: [...matchShould, ...combinedShould.term], minimum_should_match: 1 } }; }; export const splitShouldClauses = ({ @@ -168,7 +220,10 @@ export const splitShouldClauses = ({ accum[chunkIndex] = { bool: { should: [], minimum_should_match: 1 } }; } // Add to the existing array element. Using mutatious push here since these arrays can get very large such as 10k+ and this is going to be a hot code spot. - accum[chunkIndex].bool.should.push(item); + if (Array.isArray(accum[chunkIndex].bool?.should)) { + (accum[chunkIndex].bool?.should as QueryDslQueryContainer[]).push(item); + } + return accum; }, []); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts index d626d8ea069c..598730c62718 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts @@ -16,6 +16,7 @@ import { enrichSignalThreatMatches, getSignalMatchesFromThreatList, } from './enrich_signal_threat_matches'; +import { getSignalValueMap } from './utils'; export const createEventSignal = async ({ alertId, @@ -50,14 +51,17 @@ export const createEventSignal = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + allowedFieldsForTermsQuery, + threatMatchedFields, }: CreateEventSignalOptions): Promise => { - const threatFilter = buildThreatMappingFilter({ + const threatFiltersFromEvents = buildThreatMappingFilter({ threatMapping, threatList: currentEventList, entryKey: 'field', + allowedFieldsForTermsQuery, }); - if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { + if (!threatFiltersFromEvents.query || threatFiltersFromEvents.query?.bool.should.length === 0) { // empty event list and we do not want to return everything as being // a hit so opt to return the existing result. ruleExecutionLogger.debug( @@ -67,13 +71,13 @@ export const createEventSignal = async ({ } else { const threatListHits = await getAllThreatListHits({ esClient: services.scopedClusterClient.asCurrentUser, - threatFilters: [...threatFilters, threatFilter], + threatFilters: [...threatFilters, threatFiltersFromEvents], query: threatQuery, language: threatLanguage, index: threatIndex, ruleExecutionLogger, threatListConfig: { - _source: [`${threatIndicatorPath}.*`, 'threat.feed.*'], + _source: [`${threatIndicatorPath}.*`, 'threat.feed.*', ...threatMatchedFields.threat], fields: undefined, }, pitId: threatPitId, @@ -83,7 +87,10 @@ export const createEventSignal = async ({ exceptionFilter, }); - const signalMatches = getSignalMatchesFromThreatList(threatListHits); + const signalMatches = getSignalMatchesFromThreatList( + threatListHits, + getSignalValueMap({ eventList: currentEventList, threatMatchedFields }) + ); const ids = signalMatches.map((item) => item.signalId); @@ -143,7 +150,7 @@ export const createEventSignal = async ({ ruleExecutionLogger.debug( `${ - threatFilter.query?.bool.should.length + threatFiltersFromEvents.query?.bool.should.length } items have completed match checks and the total times to search were ${ result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) ' }ms` diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index f4985db71818..8773abbdae84 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -6,13 +6,13 @@ */ import { buildThreatMappingFilter } from './build_threat_mapping_filter'; - import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; import { buildReasonMessageForThreatMatchAlert } from '../reason_formatters'; import type { CreateThreatSignalOptions } from './types'; import type { SearchAfterAndBulkCreateReturnType } from '../types'; +import { buildThreatEnrichment } from './build_threat_enrichment'; export const createThreatSignal = async ({ alertId, bulkCreate, @@ -30,7 +30,6 @@ export const createThreatSignal = async ({ savedId, searchAfterSize, services, - threatEnrichment, threatMapping, tuple, type, @@ -40,11 +39,20 @@ export const createThreatSignal = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + threatFilters, + threatIndex, + threatIndicatorPath, + threatLanguage, + threatPitId, + threatQuery, + reassignThreatPitId, + allowedFieldsForTermsQuery, }: CreateThreatSignalOptions): Promise => { const threatFilter = buildThreatMappingFilter({ threatMapping, threatList: currentThreatList, entryKey: 'value', + allowedFieldsForTermsQuery, }); if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { @@ -70,6 +78,22 @@ export const createThreatSignal = async ({ `${threatFilter.query?.bool.should.length} indicator items are being checked for existence of matches` ); + const threatEnrichment = buildThreatEnrichment({ + ruleExecutionLogger, + services, + threatFilters, + threatIndex, + threatIndicatorPath, + threatLanguage, + threatQuery, + pitId: threatPitId, + reassignPitId: reassignThreatPitId, + listClient, + exceptionFilter, + threatMapping, + runtimeMappings, + }); + const result = await searchAfterAndBulkCreate({ buildReasonMessage: buildReasonMessageForThreatMatchAlert, bulkCreate, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 2bdd9533f1ea..a73485a4fd37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -17,8 +17,13 @@ import type { import { createThreatSignal } from './create_threat_signal'; import { createEventSignal } from './create_event_signal'; import type { SearchAfterAndBulkCreateReturnType } from '../types'; -import { buildExecutionIntervalValidator, combineConcurrentResults } from './utils'; -import { buildThreatEnrichment } from './build_threat_enrichment'; +import { + buildExecutionIntervalValidator, + combineConcurrentResults, + getMatchedFields, +} from './utils'; +import { getAllowedFieldsForTermQuery } from './get_allowed_fields_for_terms_query'; + import { getEventCount, getEventList } from './get_event_count'; import { getMappingFilters } from './get_mapping_filters'; import { THREAT_PIT_KEEP_ALIVE } from '../../../../../common/cti/constants'; @@ -55,6 +60,15 @@ export const createThreatSignals = async ({ exceptionFilter, unprocessedExceptions, }: CreateThreatSignalsOptions): Promise => { + const threatMatchedFields = getMatchedFields(threatMapping); + const allowedFieldsForTermsQuery = await getAllowedFieldsForTermQuery({ + services, + threatMatchedFields, + inputIndex, + threatIndex, + ruleExecutionLogger, + }); + const params = completeRule.ruleParams; ruleExecutionLogger.debug('Indicator matching rule starting'); const perPage = concurrentSearches * itemsPerSearch; @@ -129,20 +143,6 @@ export const createThreatSignals = async ({ _source: false, }; - const threatEnrichment = buildThreatEnrichment({ - ruleExecutionLogger, - services, - threatFilters: allThreatFilters, - threatIndex, - threatIndicatorPath, - threatLanguage, - threatQuery, - pitId: threatPitId, - reassignPitId: reassignThreatPitId, - listClient, - exceptionFilter, - }); - const createSignals = async ({ getDocumentList, createSignal, @@ -224,7 +224,6 @@ export const createThreatSignals = async ({ savedId, searchAfterSize, services, - threatEnrichment, threatFilters: allThreatFilters, threatIndex, threatIndicatorPath, @@ -240,6 +239,8 @@ export const createThreatSignals = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + allowedFieldsForTermsQuery, + threatMatchedFields, }), }); } else { @@ -281,7 +282,6 @@ export const createThreatSignals = async ({ savedId, searchAfterSize, services, - threatEnrichment, threatMapping, tuple, type, @@ -291,6 +291,14 @@ export const createThreatSignals = async ({ secondaryTimestamp, exceptionFilter, unprocessedExceptions, + threatFilters: allThreatFilters, + threatIndex, + threatIndicatorPath, + threatLanguage, + threatPitId, + threatQuery, + reassignThreatPitId, + allowedFieldsForTermsQuery, }), }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts index 738eba89fe22..ef68285669de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.mock.ts @@ -15,6 +15,7 @@ export const getNamedQueryMock = ( index: 'index', field: 'field', value: 'value', + queryType: 'mq', ...overrides, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index bb341d7e5423..974b0e00a7ce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -19,7 +19,12 @@ import { MAX_NUMBER_OF_SIGNAL_MATCHES, } from './enrich_signal_threat_matches'; import { getNamedQueryMock, getSignalHitMock } from './enrich_signal_threat_matches.mock'; -import type { GetMatchedThreats, ThreatListItem, ThreatMatchNamedQuery } from './types'; +import type { + GetMatchedThreats, + ThreatListItem, + ThreatMatchNamedQuery, + SignalMatch, +} from './types'; import { encodeThreatMatchNamedQuery } from './utils'; describe('groupAndMergeSignalMatches', () => { @@ -480,6 +485,7 @@ describe('enrichSignalThreatMatches', () => { let getMatchedThreats: GetMatchedThreats; let matchedQuery: string; let indicatorPath: string; + let signalMatches: SignalMatch[]; beforeEach(() => { indicatorPath = 'threat.indicator'; @@ -502,6 +508,19 @@ describe('enrichSignalThreatMatches', () => { value: 'threat.indicator.domain', }) ); + signalMatches = [ + { + signalId: '_id', + queries: [ + getNamedQueryMock({ + id: '123', + index: 'indicator_index', + field: 'event.domain', + value: 'threat.indicator.domain', + }), + ], + }, + ]; }); it('performs no enrichment if there are no signals', async () => { @@ -509,7 +528,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + [] ); expect(enrichedSignals).toEqual([]); @@ -528,7 +548,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + signalMatches ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -562,7 +583,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + signalMatches ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -600,7 +622,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + signalMatches ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -637,7 +660,7 @@ describe('enrichSignalThreatMatches', () => { }); const signals: SignalSourceHit[] = [signalHit]; await expect(() => - enrichSignalThreatMatches(signals, getMatchedThreats, indicatorPath) + enrichSignalThreatMatches(signals, getMatchedThreats, indicatorPath, signalMatches) ).rejects.toThrowError('Expected threat field to be an object, but found: whoops'); }); @@ -656,14 +679,13 @@ describe('enrichSignalThreatMatches', () => { }, }), ]; - matchedQuery = encodeThreatMatchNamedQuery( - getNamedQueryMock({ - id: '123', - index: 'custom_index', - field: 'event.domain', - value: 'custom_threat.custom_indicator.domain', - }) - ); + const namedQuery = getNamedQueryMock({ + id: '123', + index: 'custom_index', + field: 'event.domain', + value: 'custom_threat.custom_indicator.domain', + }); + matchedQuery = encodeThreatMatchNamedQuery(namedQuery); const signalHit = getSignalHitMock({ _source: { event: { @@ -676,7 +698,8 @@ describe('enrichSignalThreatMatches', () => { const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - 'custom_threat.custom_indicator' + 'custom_threat.custom_indicator', + [{ signalId: '_id', queries: [namedQuery] }] ); const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); @@ -727,6 +750,12 @@ describe('enrichSignalThreatMatches', () => { }, matched_queries: [matchedQuery], }); + const otherMatchQuery = getNamedQueryMock({ + id: '456', + index: 'other_custom_index', + field: 'event.other', + value: 'threat.indicator.domain', + }); const otherSignalHit = getSignalHitMock({ _id: 'signal123', _source: { @@ -735,22 +764,27 @@ describe('enrichSignalThreatMatches', () => { other: 'test_val', }, }, - matched_queries: [ - encodeThreatMatchNamedQuery( - getNamedQueryMock({ - id: '456', - index: 'other_custom_index', - field: 'event.other', - value: 'threat.indicator.domain', - }) - ), - ], + matched_queries: [encodeThreatMatchNamedQuery(otherMatchQuery)], }); const signals: SignalSourceHit[] = [signalHit, otherSignalHit]; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, - indicatorPath + indicatorPath, + [ + { + signalId: 'signal123', + queries: [ + getNamedQueryMock({ + id: '123', + index: 'indicator_index', + field: 'event.domain', + value: 'threat.indicator.domain', + }), + otherMatchQuery, + ], + }, + ] ); expect(enrichedSignals).toHaveLength(1); @@ -834,6 +868,167 @@ describe('getSignalMatchesFromThreatList', () => { value: 'threat.indicator.domain', index: 'threat_index', id: 'threatId', + queryType: 'mq', + }, + ]; + + expect(signalMatches).toEqual([ + { + signalId: 'signalId1', + queries, + }, + { + signalId: 'signalId2', + queries, + }, + ]); + }); + + it('return empty array for terms query if there no signalValueMap', () => { + const signalMatches = getSignalMatchesFromThreatList([ + getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }), + ]); + + expect(signalMatches).toEqual([]); + }); + + it('return empty array for terms query if there wrong value in threat indicator', () => { + const threat = getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }); + + threat._source = { + ...threat._source, + threat: { + indicator: { + domain: { a: 'b' }, + }, + }, + }; + + const signalValueMap = { + 'event.domain': { + domain_1: ['signalId1', 'signalId2'], + }, + }; + + const signalMatches = getSignalMatchesFromThreatList([threat], signalValueMap); + + expect(signalMatches).toEqual([]); + }); + + it('return signal matches from threat indicators for termsQuery', () => { + const threat = getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }); + + threat._source = { + ...threat._source, + threat: { + indicator: { + domain: 'domain_1', + }, + }, + }; + + const signalValueMap = { + 'event.domain': { + domain_1: ['signalId1', 'signalId2'], + }, + }; + + const signalMatches = getSignalMatchesFromThreatList([threat], signalValueMap); + + const queries = [ + { + field: 'event.domain', + value: 'threat.indicator.domain', + index: 'threat_index', + id: 'threatId', + queryType: 'tq', + }, + ]; + + expect(signalMatches).toEqual([ + { + signalId: 'signalId1', + queries, + }, + { + signalId: 'signalId2', + queries, + }, + ]); + }); + + it('return signal matches from threat indicators which has array values for termsQuery', () => { + const threat = getThreatListItemMock({ + _id: 'threatId', + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + value: 'threat.indicator.domain', + field: 'event.domain', + queryType: 'tq', + }) + ), + ], + }); + + threat._source = { + ...threat._source, + threat: { + indicator: { + domain: ['domain_3', 'domain_1', 'domain_2'], + }, + }, + }; + + const signalValueMap = { + 'event.domain': { + domain_1: ['signalId1'], + domain_2: ['signalId2'], + }, + }; + + const signalMatches = getSignalMatchesFromThreatList([threat], signalValueMap); + + const queries = [ + { + field: 'event.domain', + value: 'threat.indicator.domain', + index: 'threat_index', + id: 'threatId', + queryType: 'tq', }, ]; @@ -876,6 +1071,7 @@ describe('getSignalMatchesFromThreatList', () => { value: 'threat.indicator.domain', index: 'threat_index', id: 'threatId', + queryType: 'mq', }; expect(signalMatches).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index e0b9d4fb6dee..3ce871283219 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -15,42 +15,86 @@ import type { ThreatListItem, ThreatMatchNamedQuery, SignalMatch, + SignalValuesMap, + ThreatTermNamedQuery, } from './types'; +import { ThreatMatchQueryType } from './types'; import { extractNamedQueries } from './utils'; export const MAX_NUMBER_OF_SIGNAL_MATCHES = 1000; export const getSignalMatchesFromThreatList = ( - threatList: ThreatListItem[] = [] + threatList: ThreatListItem[] = [], + signalValueMap?: SignalValuesMap ): SignalMatch[] => { const signalMap: { [key: string]: ThreatMatchNamedQuery[] } = {}; + const addSignalValueToMap = ({ + id, + threatHit, + query, + }: { + id: string; + threatHit: ThreatListItem; + query: ThreatMatchNamedQuery | ThreatTermNamedQuery; + }) => { + if (!signalMap[id]) { + signalMap[id] = []; + } - threatList.forEach((threatHit) => - extractNamedQueries(threatHit).forEach((item) => { - const signalId = item.id; - if (!signalId) { - return; - } - - if (!signalMap[signalId]) { - signalMap[signalId] = []; - } + // creating map of signal with large number of threats could lead to out of memory Kibana crash + // large number of threats also can cause signals bulk create failure due too large payload (413) + // large number of threats significantly slower alert details page render + // so, its number is limited to MAX_NUMBER_OF_SIGNAL_MATCHES + // more details https://github.com/elastic/kibana/issues/143595#issuecomment-1335433592 + if (signalMap[id].length >= MAX_NUMBER_OF_SIGNAL_MATCHES) { + return; + } - // creating map of signal with large number of threats could lead to out of memory Kibana crash - // large number of threats also can cause signals bulk create failure due too large payload (413) - // large number of threats significantly slower alert details page render - // so, its number is limited to MAX_NUMBER_OF_SIGNAL_MATCHES - // more details https://github.com/elastic/kibana/issues/143595#issuecomment-1335433592 - if (signalMap[signalId].length >= MAX_NUMBER_OF_SIGNAL_MATCHES) { - return; + signalMap[id].push({ + id: threatHit._id, + index: threatHit._index, + field: query.field, + value: query.value, + queryType: query.queryType, + }); + }; + threatList.forEach((threatHit) => + extractNamedQueries(threatHit).forEach((query) => { + const signalId = query.id; + + if (query.queryType === ThreatMatchQueryType.term) { + const threatValue = get(threatHit?._source, query.value); + let values; + if (Array.isArray(threatValue)) { + values = threatValue; + } else { + values = [threatValue]; + } + + values.forEach((value) => { + if (value && signalValueMap) { + const ids = signalValueMap[query.field][value?.toString()]; + + ids?.forEach((id: string) => { + addSignalValueToMap({ + id, + threatHit, + query, + }); + }); + } + }); + } else { + if (!signalId) { + return; + } + + addSignalValueToMap({ + id: signalId, + threatHit, + query, + }); } - - signalMap[signalId].push({ - id: threatHit._id, - index: threatHit._index, - field: item.field, - value: item.value, - }); }) ); @@ -123,19 +167,13 @@ export const enrichSignalThreatMatches = async ( signals: SignalSourceHit[], getMatchedThreats: GetMatchedThreats, indicatorPath: string, - signalMatchesArg?: SignalMatch[] + signalMatches: SignalMatch[] ): Promise => { if (signals.length === 0) { return signals; } const uniqueHits = groupAndMergeSignalMatches(signals); - const signalMatches: SignalMatch[] = signalMatchesArg - ? signalMatchesArg - : uniqueHits.map((signalHit) => ({ - signalId: signalHit._id, - queries: extractNamedQueries(signalHit), - })); const matchedThreatIds = [ ...new Set( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.test.ts new file mode 100644 index 000000000000..b13b5d23278b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.test.ts @@ -0,0 +1,144 @@ +/* + * 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 { IndicesGetFieldMappingResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; + +import { + getAllowedFieldForTermQueryFromMapping, + getAllowedFieldsForTermQuery, +} from './get_allowed_fields_for_terms_query'; + +const indexMapping = { + 'source-index': { + mappings: { + 'host.name': { + full_name: 'host.name', + mapping: { + name: { + type: 'keyword', + }, + }, + }, + 'url.full': { + full_name: 'url.full', + mapping: { + full: { + type: 'keyword', + }, + }, + }, + 'source.range': { + full_name: 'source.range', + mapping: { + range: { + type: 'ip_range', + }, + }, + }, + }, + }, + 'other-source-index': { + mappings: { + 'host.name': { + full_name: 'host.name', + mapping: { + name: { + type: 'keyword', + }, + }, + }, + 'host.ip': { + full_name: 'host.ip', + mapping: { + name: { + type: 'ip', + }, + }, + }, + }, + }, +}; + +describe('get_allowed_fields_for_terms_query copy', () => { + describe('getAllowedFieldForTermQueryFromMapping', () => { + it('should return map of fields allowed for term query', () => { + const result = getAllowedFieldForTermQueryFromMapping( + indexMapping as IndicesGetFieldMappingResponse + ); + expect(result).toEqual({ + 'host.ip': true, + 'url.full': true, + 'host.name': true, + }); + }); + it('should disable fields if in one index type not supported', () => { + const result = getAllowedFieldForTermQueryFromMapping({ + 'new-source-index': { + mappings: { + 'host.name': { + full_name: 'host.name', + mapping: { + name: { + type: 'text', + }, + }, + }, + }, + }, + ...indexMapping, + } as IndicesGetFieldMappingResponse); + expect(result).toEqual({ + 'host.ip': true, + 'url.full': true, + }); + }); + }); + + describe('getlAllowedFieldsForTermQuery', () => { + let alertServices: RuleExecutorServicesMock; + let ruleExecutionLogger: ReturnType; + + beforeEach(() => { + alertServices = alertsMock.createRuleExecutorServices(); + alertServices.scopedClusterClient.asCurrentUser.indices.getFieldMapping.mockResolvedValue( + indexMapping as IndicesGetFieldMappingResponse + ); + ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); + }); + + it('should return map of fields allowed for term query for source and threat indices', async () => { + const threatMatchedFields = { + source: ['host.name', 'url.full'], + threat: ['host.name', 'url.full'], + }; + const threatIndex = ['threat-index']; + const inputIndex = ['source-index']; + + const result = await getAllowedFieldsForTermQuery({ + threatMatchedFields, + services: alertServices, + threatIndex, + inputIndex, + ruleExecutionLogger, + }); + expect(result).toEqual({ + source: { + 'host.ip': true, + 'url.full': true, + 'host.name': true, + }, + threat: { + 'host.ip': true, + 'url.full': true, + 'host.name': true, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.ts new file mode 100644 index 000000000000..bde234e2bdc2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_allowed_fields_for_terms_query.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IndicesGetFieldMappingResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { AllowedFieldsForTermsQuery, GetAllowedFieldsForTermQuery } from './types'; + +const allowedFieldTypes = ['keyword', 'constant_keyword', 'wildcard', 'ip']; + +/* + * Return map of fields allowed for term query + */ +export const getAllowedFieldForTermQueryFromMapping = ( + indexMapping: IndicesGetFieldMappingResponse +): Record => { + const result: Record = {}; + const notAllowedFields: string[] = []; + + const indices = Object.values(indexMapping); + indices.forEach((index) => { + Object.entries(index.mappings).forEach(([field, fieldValue]) => { + Object.values(fieldValue.mapping).forEach((mapping) => { + const fieldType = mapping?.type; + if (!fieldType) return; + + if (allowedFieldTypes.includes(fieldType) && !notAllowedFields.includes(field)) { + result[field] = true; + } else { + notAllowedFields.push(field); + // if we the field allowed in one index, but not allowed in another, we should delete it from result + delete result[field]; + } + }); + }); + }); + + return result; +}; + +/** + * Return map of fields allowed for term query for source and threat indices + */ +export const getAllowedFieldsForTermQuery = async ({ + threatMatchedFields, + services, + threatIndex, + inputIndex, + ruleExecutionLogger, +}: GetAllowedFieldsForTermQuery): Promise => { + let allowedFieldsForTermsQuery = { source: {}, threat: {} }; + try { + const [sourceFieldsMapping, threatFieldsMapping] = await Promise.all([ + services.scopedClusterClient.asCurrentUser.indices.getFieldMapping({ + index: inputIndex, + fields: threatMatchedFields.source, + }), + services.scopedClusterClient.asCurrentUser.indices.getFieldMapping({ + index: threatIndex, + fields: threatMatchedFields.threat, + }), + ]); + + allowedFieldsForTermsQuery = { + source: getAllowedFieldForTermQueryFromMapping(sourceFieldsMapping), + threat: getAllowedFieldForTermQueryFromMapping(threatFieldsMapping), + }; + } catch (e) { + ruleExecutionLogger.debug(`Can't get allowed fields for terms query: ${e}`); + return allowedFieldsForTermsQuery; + } + + return allowedFieldsForTermsQuery; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts index 32c2f00af64e..c17696fbddf2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts @@ -11,7 +11,7 @@ import { getQueryFilter } from '../get_query_filter'; import { singleSearchAfter } from '../single_search_after'; import { buildEventsSearchQuery } from '../build_events_query'; -export const MAX_PER_PAGE = 3000; +export const MAX_PER_PAGE = 9000; export const getEventList = async ({ services, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 84cf1142e9e9..63cf588ae43a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -17,6 +17,7 @@ import type { LanguageOrUndefined, Type, } from '@kbn/securitysolution-io-ts-alerting-types'; +import type { QueryDslBoolQuery } from '@elastic/elasticsearch/lib/api/types'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { OpenPointInTimeResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ListClient } from '@kbn/lists-plugin/server'; @@ -32,7 +33,6 @@ import type { BulkCreate, RuleRangeTuple, SearchAfterAndBulkCreateReturnType, - SignalsEnrichment, WrapHits, OverrideBodyQuery, } from '../types'; @@ -91,7 +91,6 @@ export interface CreateThreatSignalOptions { savedId: string | undefined; searchAfterSize: number; services: RuleExecutorServices; - threatEnrichment: SignalsEnrichment; threatMapping: ThreatMapping; tuple: RuleRangeTuple; type: Type; @@ -101,6 +100,15 @@ export interface CreateThreatSignalOptions { secondaryTimestamp?: string; exceptionFilter: Filter | undefined; unprocessedExceptions: ExceptionListItemSchema[]; + threatFilters: unknown[]; + threatIndex: ThreatIndex; + threatIndicatorPath: ThreatIndicatorPath; + threatLanguage: ThreatLanguageOrUndefined; + threatQuery: ThreatQuery; + perPage?: number; + threatPitId: OpenPointInTimeResponse['id']; + reassignThreatPitId: (newPitId: OpenPointInTimeResponse['id'] | undefined) => void; + allowedFieldsForTermsQuery: AllowedFieldsForTermsQuery; } export interface CreateEventSignalOptions { @@ -120,7 +128,6 @@ export interface CreateEventSignalOptions { savedId: string | undefined; searchAfterSize: number; services: RuleExecutorServices; - threatEnrichment: SignalsEnrichment; tuple: RuleRangeTuple; type: Type; wrapHits: WrapHits; @@ -138,6 +145,8 @@ export interface CreateEventSignalOptions { secondaryTimestamp?: string; exceptionFilter: Filter | undefined; unprocessedExceptions: ExceptionListItemSchema[]; + allowedFieldsForTermsQuery: AllowedFieldsForTermsQuery; + threatMatchedFields: ThreatMatchedFields; } type EntryKey = 'field' | 'value'; @@ -146,6 +155,7 @@ export interface BuildThreatMappingFilterOptions { threatList: ThreatListItem[]; threatMapping: ThreatMapping; entryKey: EntryKey; + allowedFieldsForTermsQuery?: AllowedFieldsForTermsQuery; } export interface FilterThreatMappingOptions { @@ -171,6 +181,7 @@ export interface BuildEntriesMappingFilterOptions { threatList: ThreatListItem[]; threatMapping: ThreatMapping; entryKey: EntryKey; + allowedFieldsForTermsQuery?: AllowedFieldsForTermsQuery; } export interface SplitShouldClausesOptions { @@ -179,7 +190,11 @@ export interface SplitShouldClausesOptions { } export interface BooleanFilter { - bool: { should: unknown[]; minimum_should_match: number }; + bool: QueryDslBoolQuery; +} + +export interface TermQuery { + terms: Record; } interface ThreatListConfig { @@ -229,12 +244,19 @@ export interface ThreatEnrichment { matched: { id: string; index: string; field: string; atomic?: string; type: string }; } -export interface ThreatMatchNamedQuery { - id: string; - index: string; +interface BaseThreatNamedQuery { field: string; value: string; + queryType: string; } +export interface ThreatMatchNamedQuery extends BaseThreatNamedQuery { + id: string; + index: string; +} + +export type ThreatTermNamedQuery = BaseThreatNamedQuery; + +export type DecodedThreatNamedQuery = BaseThreatNamedQuery & { id?: string; index?: string }; export type GetMatchedThreats = (ids: string[]) => Promise; @@ -250,6 +272,8 @@ export interface BuildThreatEnrichmentOptions { reassignPitId: (newPitId: OpenPointInTimeResponse['id'] | undefined) => void; listClient: ListClient; exceptionFilter: Filter | undefined; + threatMapping: ThreatMapping; + runtimeMappings: estypes.MappingRuntimeFields | undefined; } export interface EventsOptions { @@ -303,3 +327,37 @@ export interface GetSortForThreatList { index: string[]; listItemIndex: string; } + +export enum ThreatMatchQueryType { + match = 'mq', + term = 'tq', +} + +export interface ThreatMatchedFields { + source: string[]; + threat: string[]; +} + +export interface AllowedFieldsForTermsQuery { + source: Record; + threat: Record; +} + +export interface SignalValuesMap { + [field: string]: { + [fieldValue: string]: string[]; + }; +} + +export interface GetAllowedFieldsForTermQuery { + services: RuleExecutorServices; + inputIndex: string[]; + threatIndex: ThreatIndex; + threatMatchedFields: ThreatMatchedFields; + ruleExecutionLogger: IRuleExecutionLogForExecutors; +} + +export interface GetSignalValuesMap { + eventList: EventItem[]; + threatMatchedFields: ThreatMatchedFields; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index 0bcdc8450a83..57eae75ba4f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -7,7 +7,7 @@ import type { SearchAfterAndBulkCreateReturnType } from '../types'; import { sampleSignalHit } from '../__mocks__/es_results'; -import type { ThreatMatchNamedQuery } from './types'; +import type { ThreatMatchNamedQuery, ThreatTermNamedQuery } from './types'; import { buildExecutionIntervalValidator, @@ -18,6 +18,8 @@ import { combineResults, decodeThreatMatchNamedQuery, encodeThreatMatchNamedQuery, + getMatchedFields, + getSignalValueMap, } from './utils'; describe('utils', () => { @@ -705,6 +707,7 @@ describe('utils', () => { index: 'index', field: 'field', value: 'value', + queryType: 'mq', }); expect(typeof encoded).toEqual('string'); @@ -718,6 +721,7 @@ describe('utils', () => { index: 'index', field: 'threat.indicator.domain', value: 'host.name', + queryType: 'mq', }; const encoded = encodeThreatMatchNamedQuery(query); @@ -727,6 +731,20 @@ describe('utils', () => { expect(decoded).toEqual(query); }); + it('can decode if some parameters not passed', () => { + const query: ThreatTermNamedQuery = { + field: 'threat.indicator.domain', + value: 'host.name', + queryType: 'tq', + }; + + const encoded = encodeThreatMatchNamedQuery(query); + const decoded = decodeThreatMatchNamedQuery(encoded); + + expect(decoded).not.toBe(query); + expect(decoded).toEqual({ ...query, id: '', index: '' }); + }); + it('raises an error if the input is invalid', () => { const badInput = 'nope'; @@ -735,18 +753,33 @@ describe('utils', () => { ); }); - it('raises an error if the query is missing a value', () => { + it('raises an error if the query is missing a value for match query', () => { const badQuery: ThreatMatchNamedQuery = { id: 'my_id', index: 'index', // @ts-expect-error field intentionally undefined field: undefined, value: 'host.name', + queryType: 'mq', + }; + const badInput = encodeThreatMatchNamedQuery(badQuery); + + expect(() => decodeThreatMatchNamedQuery(badInput)).toThrowError( + 'Decoded query is invalid. Decoded value: {"id":"my_id","index":"index","field":"","value":"host.name","queryType":"mq"}' + ); + }); + + it('raises an error if the query is invalid a value for term query', () => { + const badQuery: ThreatTermNamedQuery = { + // @ts-expect-error field intentionally undefined + field: undefined, + value: 'host.name', + queryType: 'tq', }; const badInput = encodeThreatMatchNamedQuery(badQuery); expect(() => decodeThreatMatchNamedQuery(badInput)).toThrowError( - 'Decoded query is invalid. Decoded value: {"id":"my_id","index":"index","field":"","value":"host.name"}' + 'Decoded query is invalid. Decoded value: {"id":"","index":"","field":"","value":"host.name","queryType":"tq"}' ); }); }); @@ -773,4 +806,120 @@ describe('utils', () => { ); }); }); + + describe('getMatchedFields', () => { + it('return empty fields if there no mappings', () => { + const fields = getMatchedFields([]); + expect(fields).toEqual({ + source: [], + threat: [], + }); + }); + + it('return fields for source and threat indecies', () => { + const fields = getMatchedFields([ + { + entries: [ + { + field: 'host.name', + type: 'mapping', + value: 'threat.indicator.host.name', + }, + ], + }, + { + entries: [ + { + field: 'source.ip', + type: 'mapping', + value: 'threat.indicator.source.ip', + }, + { + field: 'url.full', + type: 'mapping', + value: 'threat.indicator.url.full', + }, + ], + }, + ]); + + expect(fields).toEqual({ + source: ['host.name', 'source.ip', 'url.full'], + threat: [ + 'threat.indicator.host.name', + 'threat.indicator.source.ip', + 'threat.indicator.url.full', + ], + }); + }); + }); + + describe('getSignalValueMap', () => { + it('return empty object if there no events', () => { + const valueMap = getSignalValueMap({ + eventList: [], + threatMatchedFields: { + source: [], + threat: [], + }, + }); + expect(valueMap).toEqual({}); + }); + + it('return empty object if there some events but no fields', () => { + const valueMap = getSignalValueMap({ + eventList: [ + { + _id: '1', + _index: 'index-1', + fields: { + 'host.name': ['host-1'], + }, + }, + ], + threatMatchedFields: { + source: [], + threat: [], + }, + }); + expect(valueMap).toEqual({}); + }); + it('return value map for event list and coresponding fields', () => { + const createEvent = (id: string, fields: Record) => ({ + _id: id, + _index: `index`, + fields, + }); + const valueMap = getSignalValueMap({ + eventList: [ + createEvent('1', { + 'host.name': ['host-1'], + 'source.ip': ['source-1'], + }), + createEvent('2', { + 'host.name': ['host-2'], + 'source.ip': ['source-2'], + }), + createEvent('3', { + 'host.name': ['host-1'], + 'source.ip': ['source-2'], + }), + ], + threatMatchedFields: { + source: ['host.name', 'source.ip', 'url.full'], + threat: [], + }, + }); + expect(valueMap).toEqual({ + 'host.name': { + 'host-1': ['1', '3'], + 'host-2': ['2'], + }, + 'source.ip': { + 'source-1': ['1'], + 'source-2': ['2', '3'], + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index a73ead0bf946..4e97fd36033d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -7,9 +7,20 @@ import moment from 'moment'; +import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; +import { get } from 'lodash'; import type { SearchAfterAndBulkCreateReturnType, SignalSourceHit } from '../types'; import { parseInterval } from '../utils'; -import type { ThreatMatchNamedQuery, ThreatListItem } from './types'; +import { ThreatMatchQueryType } from './types'; +import type { + ThreatListItem, + ThreatMatchedFields, + ThreatTermNamedQuery, + DecodedThreatNamedQuery, + SignalValuesMap, + GetSignalValuesMap, + ThreatMatchNamedQuery, +} from './types'; /** * Given two timers this will take the max of each and add them to each other and return that addition. @@ -129,21 +140,32 @@ export const combineConcurrentResults = ( }; const separator = '__SEP__'; -export const encodeThreatMatchNamedQuery = ({ - id, - index, - field, - value, -}: ThreatMatchNamedQuery): string => { - return [id, index, field, value].join(separator); +export const encodeThreatMatchNamedQuery = ( + query: ThreatMatchNamedQuery | ThreatTermNamedQuery +): string => { + const { field, value, queryType } = query; + let id; + let index; + if ('id' in query) { + id = query.id; + index = query.index; + } + + return [id, index, field, value, queryType].join(separator); }; -export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQuery => { +export const decodeThreatMatchNamedQuery = (encoded: string): DecodedThreatNamedQuery => { const queryValues = encoded.split(separator); - const [id, index, field, value] = queryValues; - const query = { id, index, field, value }; - - if (queryValues.length !== 4 || !queryValues.every(Boolean)) { + const [id, index, field, value, queryType] = queryValues; + const query = { id, index, field, value, queryType }; + let isValidQuery = false; + if (queryType === ThreatMatchQueryType.match) { + isValidQuery = queryValues.length === 5 && queryValues.every(Boolean); + } + if (queryType === ThreatMatchQueryType.term) { + isValidQuery = Boolean(field && value); + } + if (!isValidQuery) { const queryString = JSON.stringify(query); throw new Error(`Decoded query is invalid. Decoded value: ${queryString}`); } @@ -153,7 +175,7 @@ export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQu export const extractNamedQueries = ( hit: SignalSourceHit | ThreatListItem -): ThreatMatchNamedQuery[] => +): DecodedThreatNamedQuery[] => hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? []; export const buildExecutionIntervalValidator: (interval: string) => () => void = (interval) => { @@ -173,3 +195,42 @@ export const buildExecutionIntervalValidator: (interval: string) => () => void = } }; }; + +/* + * Return list of fields by type used for matching in IM rule + */ +export const getMatchedFields = (threatMapping: ThreatMapping): ThreatMatchedFields => + threatMapping.reduce( + (acc: ThreatMatchedFields, val) => { + val.entries.forEach((mapping) => { + if (!acc.source.includes(mapping.field)) { + acc.source.push(mapping.field); + } + if (!acc.threat.includes(mapping.value)) { + acc.threat.push(mapping.value); + } + }); + return acc; + }, + { source: [], threat: [] } + ); + +export const getSignalValueMap = ({ + eventList, + threatMatchedFields, +}: GetSignalValuesMap): SignalValuesMap => + eventList.reduce((acc, event) => { + threatMatchedFields.source.forEach((field) => { + const fieldValue = get(event.fields, field)?.[0]; + if (!fieldValue) return; + + if (!acc[field]) { + acc[field] = {}; + } + if (!acc[field][fieldValue]) { + acc[field][fieldValue] = []; + } + acc[field][fieldValue].push(event._id); + }); + return acc; + }, {}); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts index dfa1f81f6c5d..dba116e46a75 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/threat_match.ts @@ -75,8 +75,8 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllAlerts(supertest, log); }); - // First test creates a real rule - remaining tests use preview API - it('should be able to execute and get 10 signals when doing a specific query', async () => { + // First 2 test creates a real rule - remaining tests use preview API + it('should be able to execute and get 10 signals when doing a specific query (terms query)', async () => { const rule: ThreatMatchRuleCreateProps = { description: 'Detecting root and admin users', name: 'Query with a rule id', @@ -257,6 +257,192 @@ export default ({ getService }: FtrProviderContext) => { }), }); }); + it('should be able to execute and get 10 signals when doing a specific query (match query)', async () => { + const rule: ThreatMatchRuleCreateProps = { + description: 'Detecting root and admin users', + name: 'Query with a rule id', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + const createdRule = await createRule(supertest, log, rule); + const alerts = await getOpenSignals(supertest, log, es, createdRule); + expect(alerts.hits.hits.length).equal(10); + const fullSource = alerts.hits.hits.find( + (signal) => + (signal._source?.[ALERT_ANCESTORS] as Ancestor[])[0].id === '7yJ-B2kBR346wHgnhlMn' + ); + const fullSignal = fullSource?._source; + if (!fullSignal) { + return expect(fullSignal).to.be.ok(); + } + expect(fullSignal).eql({ + ...fullSignal, + '@timestamp': fullSignal['@timestamp'], + agent: { + ephemeral_id: '1b4978a0-48be-49b1-ac96-323425b389ab', + hostname: 'zeek-sensor-amsterdam', + id: 'e52588e6-7aa3-4c89-a2c4-d6bc5c286db1', + type: 'auditbeat', + version: '8.0.0', + }, + auditd: { + data: { + hostname: '46.101.47.213', + op: 'PAM:bad_ident', + terminal: 'ssh', + }, + message_type: 'user_err', + result: 'fail', + sequence: 2267, + session: 'unset', + summary: { + actor: { + primary: 'unset', + secondary: 'root', + }, + how: '/usr/sbin/sshd', + object: { + primary: 'ssh', + secondary: '46.101.47.213', + type: 'user-session', + }, + }, + }, + cloud: { + instance: { + id: '133551048', + }, + provider: 'digitalocean', + region: 'ams3', + }, + ecs: { + version: '1.0.0-beta2', + }, + ...flattenWithPrefix('event', { + action: 'error', + category: 'user-login', + module: 'auditd', + kind: 'signal', + }), + host: { + architecture: 'x86_64', + containerized: false, + hostname: 'zeek-sensor-amsterdam', + id: '2ce8b1e7d69e4a1d9c6bcddc473da9d9', + name: 'zeek-sensor-amsterdam', + os: { + codename: 'bionic', + family: 'debian', + kernel: '4.15.0-45-generic', + name: 'Ubuntu', + platform: 'ubuntu', + version: '18.04.2 LTS (Bionic Beaver)', + }, + }, + network: { + direction: 'incoming', + }, + process: { + executable: '/usr/sbin/sshd', + pid: 32739, + }, + service: { + type: 'auditd', + }, + source: { + ip: '46.101.47.213', + }, + user: { + audit: { + id: 'unset', + }, + id: '0', + name: 'root', + }, + [ALERT_ANCESTORS]: [ + { + id: '7yJ-B2kBR346wHgnhlMn', + type: 'event', + index: 'auditbeat-8.0.0-2019.02.19-000001', + depth: 0, + }, + ], + [ALERT_DEPTH]: 1, + [ALERT_ORIGINAL_EVENT_ACTION]: 'error', + [ALERT_ORIGINAL_EVENT_CATEGORY]: 'user-login', + [ALERT_ORIGINAL_EVENT_MODULE]: 'auditd', + [ALERT_ORIGINAL_TIME]: fullSignal[ALERT_ORIGINAL_TIME], + [ALERT_REASON]: + 'user-login event with source 46.101.47.213 by root on zeek-sensor-amsterdam created high alert Query with a rule id.', + [ALERT_RULE_UUID]: fullSignal[ALERT_RULE_UUID], + [ALERT_STATUS]: 'active', + [ALERT_UUID]: fullSignal[ALERT_UUID], + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['default'], + [VERSION]: fullSignal[VERSION], + threat: { + enrichments: get(fullSignal, 'threat.enrichments'), + }, + ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { + actions: [], + author: [], + category: 'Indicator Match Rule', + consumer: 'siem', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + exceptions_list: [], + false_positives: [], + from: '1900-01-01T00:00:00.000Z', + immutable: false, + interval: '5m', + max_signals: 100, + name: 'Query with a rule id', + producer: 'siem', + references: [], + risk_score: 55, + risk_score_mapping: [], + rule_type_id: 'siem.indicatorRule', + severity: 'high', + severity_mapping: [], + tags: [], + threat: [], + to: 'now', + type: 'threat_match', + updated_at: fullSignal[ALERT_RULE_UPDATED_AT], + updated_by: 'elastic', + uuid: fullSignal[ALERT_RULE_UUID], + version: 1, + }), + }); + }); it('should return 0 matches if the mapping does not match against anything in the mapping', async () => { const rule: ThreatMatchRuleCreateProps = { From 7b842280b8a6bbf540db27480a40ae899779aa79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 2 Feb 2023 12:12:36 -0500 Subject: [PATCH 28/35] [Obs] fixing news feed by using dynamic kibana version (#150053) closes https://github.com/elastic/kibana/issues/149986 Screenshot 2023-02-01 at 9 03 40 AM --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/application.test.tsx | 1 + .../public/application/index.tsx | 10 ++++++++- .../observability/public/application/types.ts | 1 + .../overview_page/overview_page.tsx | 6 ++++- x-pack/plugins/observability/public/plugin.ts | 2 ++ .../public/services/get_news_feed.test.ts | 4 ++-- .../public/services/get_news_feed.ts | 22 +++++++++++++++++-- 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index 5c5c6a24a03d..3320022c12b8 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -91,6 +91,7 @@ describe('renderApp', () => { }, reportUiCounter: jest.fn(), }, + kibanaVersion: '8.7.0', }); unmount(); }).not.toThrowError(); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 283cc195089a..7edbb74b80c8 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -53,6 +53,7 @@ export const renderApp = ({ ObservabilityPageTemplate, usageCollection, isDev, + kibanaVersion, }: { core: CoreStart; config: ConfigSchema; @@ -62,6 +63,7 @@ export const renderApp = ({ ObservabilityPageTemplate: React.ComponentType; usageCollection: UsageCollectionSetup; isDev?: boolean; + kibanaVersion: string; }) => { const { element, history, theme$ } = appMountParameters; const i18nCore = core.i18n; @@ -83,7 +85,13 @@ export const renderApp = ({ ().services; const { ObservabilityPageTemplate } = usePluginContext(); @@ -66,7 +67,10 @@ export function OverviewPage() { }, ]); - const { data: newsFeed } = useFetcher(() => getNewsFeed({ http }), [http]); + const { data: newsFeed } = useFetcher( + () => getNewsFeed({ http, kibanaVersion }), + [http, kibanaVersion] + ); const { hasAnyData, isAllRequestsComplete } = useHasData(); const { trackMetric } = useOverviewMetrics({ hasAnyData }); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 17a21f41301b..332e481f0ec5 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -187,6 +187,7 @@ export class Plugin const category = DEFAULT_APP_CATEGORIES.observability; const euiIconType = 'logoObservability'; const config = this.initContext.config.get(); + const kibanaVersion = this.initContext.env.packageInfo.version; createCallObservabilityApi(coreSetup.http); @@ -211,6 +212,7 @@ export class Plugin ObservabilityPageTemplate: navigation.PageTemplate, usageCollection: pluginsSetup.usageCollection, isDev: this.initContext.env.mode.dev, + kibanaVersion, }); }; diff --git a/x-pack/plugins/observability/public/services/get_news_feed.test.ts b/x-pack/plugins/observability/public/services/get_news_feed.test.ts index 3a35e8716672..59178bd841be 100644 --- a/x-pack/plugins/observability/public/services/get_news_feed.test.ts +++ b/x-pack/plugins/observability/public/services/get_news_feed.test.ts @@ -25,7 +25,7 @@ describe('getNewsFeed', () => { }, } as unknown as HttpSetup; - const newsFeed = await getNewsFeed({ http }); + const newsFeed = await getNewsFeed({ http, kibanaVersion: '8.7.0' }); expect(newsFeed.items).toEqual([]); }); it('Returns array with the news feed', async () => { @@ -92,7 +92,7 @@ describe('getNewsFeed', () => { }, } as unknown as HttpSetup; - const newsFeed = await getNewsFeed({ http }); + const newsFeed = await getNewsFeed({ http, kibanaVersion: '8.7.0' }); expect(newsFeed.items.length).toEqual(3); }); }); diff --git a/x-pack/plugins/observability/public/services/get_news_feed.ts b/x-pack/plugins/observability/public/services/get_news_feed.ts index 31c7c6cd30ea..8916cffbd149 100644 --- a/x-pack/plugins/observability/public/services/get_news_feed.ts +++ b/x-pack/plugins/observability/public/services/get_news_feed.ts @@ -6,6 +6,7 @@ */ import type { HttpSetup } from '@kbn/core/public'; +import semverCoerce from 'semver/functions/coerce'; export interface NewsItem { title: { en: string }; @@ -17,10 +18,27 @@ export interface NewsItem { interface NewsFeed { items: NewsItem[]; } +/** + * Removes the suffix that is sometimes appended to the Kibana version, + * (e.g. `8.0.0-SNAPSHOT-rc1`), which is typically only seen in non-production + * environments + */ +const removeSuffixFromVersion = (kibanaVersion?: string) => + semverCoerce(kibanaVersion)?.version ?? kibanaVersion; -export async function getNewsFeed({ http }: { http: HttpSetup }): Promise { +export async function getNewsFeed({ + http, + kibanaVersion, +}: { + http: HttpSetup; + kibanaVersion: string; +}): Promise { try { - return await http.get('https://feeds.elastic.co/observability-solution/v8.0.0.json'); + return await http.get( + `https://feeds.elastic.co/observability-solution/v${removeSuffixFromVersion( + kibanaVersion + )}.json` + ); } catch (e) { console.error('Error while fetching news feed', e); return { items: [] }; From f5af84f3818ad13729aca2d1a6d81fc182e4a7bb Mon Sep 17 00:00:00 2001 From: Nav <13634519+navarone-feekery@users.noreply.github.com> Date: Thu, 2 Feb 2023 18:24:16 +0100 Subject: [PATCH 29/35] [Enterprise Search] Add preferences to connectors (#150165) ## Summary Add support for new field `preferences` in `.elastic-connectors`. --- x-pack/plugins/enterprise_search/common/types/connectors.ts | 5 +++++ .../__mocks__/search_indices.mock.ts | 2 ++ .../enterprise_search_content/__mocks__/view_index.mock.ts | 2 ++ .../server/index_management/setup_indices.test.ts | 5 +++++ .../server/index_management/setup_indices.ts | 5 +++++ .../server/lib/connectors/add_connector.test.ts | 3 +++ .../enterprise_search/server/lib/connectors/add_connector.ts | 1 + .../server/lib/connectors/start_sync.test.ts | 2 ++ .../lib/connectors/update_connector_scheduling.test.ts | 2 ++ 9 files changed, 27 insertions(+) diff --git a/x-pack/plugins/enterprise_search/common/types/connectors.ts b/x-pack/plugins/enterprise_search/common/types/connectors.ts index 87ccbad824e1..2913423b4628 100644 --- a/x-pack/plugins/enterprise_search/common/types/connectors.ts +++ b/x-pack/plugins/enterprise_search/common/types/connectors.ts @@ -27,6 +27,10 @@ export interface CustomScheduling { export type ConnectorCustomScheduling = Record; +export interface ConnectorPreferences extends Record { + extract_full_html?: boolean | null; +} + export enum ConnectorStatus { CREATED = 'created', NEEDS_CONFIGURATION = 'needs_configuration', @@ -150,6 +154,7 @@ export interface Connector { last_synced: string | null; name: string; pipeline?: IngestPipelineParams | null; + preferences: ConnectorPreferences; scheduling: { enabled: boolean; interval: string; // crontab syntax diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts index 955cf219a801..c9ccc1c0e7e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts @@ -105,6 +105,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'connector', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', @@ -200,6 +201,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'crawler', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts index 78ef10664abc..05211e48bf13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts @@ -115,6 +115,7 @@ export const connectorIndex: ConnectorViewIndex = { last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'connector', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', @@ -214,6 +215,7 @@ export const crawlerIndex: CrawlerViewIndex = { last_sync_status: SyncStatus.COMPLETED, last_synced: null, name: 'crawler', + preferences: { extract_full_html: false }, scheduling: { enabled: false, interval: '', diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts index 6ba4d9484ea8..8c8c16e9291f 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.test.ts @@ -140,6 +140,11 @@ describe('Setup Indices', () => { run_ml_inference: { type: 'boolean' }, }, }, + preferences: { + properties: { + extract_full_html: { type: 'boolean' }, + }, + }, scheduling: { properties: { enabled: { type: 'boolean' }, diff --git a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts index 10ff75fcb566..488b0fc10930 100644 --- a/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts +++ b/x-pack/plugins/enterprise_search/server/index_management/setup_indices.ts @@ -130,6 +130,11 @@ const connectorMappingsProperties: Record = { run_ml_inference: { type: 'boolean' }, }, }, + preferences: { + properties: { + extract_full_html: { type: 'boolean' }, + }, + }, scheduling: { properties: { enabled: { type: 'boolean' }, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts index e6584c0a8b20..c3b52f53857d 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.test.ts @@ -154,6 +154,7 @@ describe('addConnector lib function', () => { reduce_whitespace: true, run_ml_inference: false, }, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, @@ -339,6 +340,7 @@ describe('addConnector lib function', () => { reduce_whitespace: true, run_ml_inference: false, }, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, @@ -446,6 +448,7 @@ describe('addConnector lib function', () => { reduce_whitespace: true, run_ml_inference: false, }, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: null, status: ConnectorStatus.CREATED, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts index 507ec3fd0bb4..abe52caf453d 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/add_connector.ts @@ -164,6 +164,7 @@ export const addConnector = async ( run_ml_inference: connectorsPipelineMeta.default_run_ml_inference, } : null, + preferences: {}, scheduling: { enabled: false, interval: '0 0 0 * * ?' }, service_type: input.service_type || null, status: ConnectorStatus.CREATED, diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts index 25956b271245..2ea8a875bd3b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts @@ -42,6 +42,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: true, interval: '1 2 3 4 5' }, service_type: null, status: 'not connected', @@ -67,6 +68,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: true, interval: '1 2 3 4 5' }, service_type: null, status: 'not connected', diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts index 2fa4c0954b24..89b936a41a88 100644 --- a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts @@ -41,6 +41,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: false, interval: '* * * * *' }, service_type: null, status: 'not connected', @@ -69,6 +70,7 @@ describe('addConnector lib function', () => { last_sync_error: null, last_sync_status: null, last_synced: null, + preferences: {}, scheduling: { enabled: true, interval: '1 2 3 4 5' }, service_type: null, status: 'not connected', From 6f79227f50de023262706070d2de65e676a8b1e2 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 2 Feb 2023 10:39:45 -0700 Subject: [PATCH 30/35] [ML] Data Frame Analytics creation wizard: ensure includes table is populated correctly on job type change (#150112) ## Summary Fixes https://github.com/elastic/kibana/issues/147824 Fixes the request body sent to the explain api - no longer sends unnecessary `analyzed_fields` property. This ensures the explain api call is successful and the includes table is populated correctly with the new job type and dependent variable. Before: ![dfa_wizard_fields_error](https://user-images.githubusercontent.com/6446462/208556732-d1ae37f0-c3eb-4983-945b-62de0af45833.gif) After: https://user-images.githubusercontent.com/6446462/216168208-a68a590d-5a45-4b4b-80eb-18b811218a00.mp4 ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../analytics_creation/components/shared/fetch_explain_data.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts index ca334a58b36c..ebf5d2dce270 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts @@ -28,6 +28,7 @@ export const fetchExplainData = async (formState: State['form']) => { try { delete jobConfig.dest; delete jobConfig.model_memory_limit; + delete jobConfig.analyzed_fields; const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( jobConfig ); From c61ff60835d4f700ae02514d0755381be199f892 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 2 Feb 2023 17:50:43 +0000 Subject: [PATCH 31/35] [Fleet] Handle package registry with no categories (#150182) ## Summary When testing locally with an EPR with no categories/subcategories, `items` is null, not a great API response but we should be able to tolerate it without breaking the page Screenshot 2023-02-02 at 11 17 22 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../epm/screens/home/hooks/use_available_packages.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx index f13fd592dacc..b8d54b45759d 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx @@ -182,22 +182,23 @@ export const useAvailablePackages = () => { ); const { - data: eprCategories, + data: eprCategoriesRes, isLoading: isLoadingCategories, error: eprCategoryLoadingError, } = useCategories(prereleaseIntegrationsEnabled); + const eprCategories = useMemo(() => eprCategoriesRes?.items || [], [eprCategoriesRes]); // Subcategories const subCategories = useMemo(() => { - return eprCategories?.items.filter((item) => item.parent_id !== undefined); - }, [eprCategories?.items]); + return eprCategories?.filter((item) => item.parent_id !== undefined); + }, [eprCategories]); const allCategories: CategoryFacet[] = useMemo(() => { const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories ? [] : mergeCategoriesAndCount( eprCategories - ? (eprCategories.items as Array<{ id: string; title: string; count: number }>) + ? (eprCategories as Array<{ id: string; title: string; count: number }>) : [], cards ); From 66e7bba497f5cf66b49c153341e419fbcc416ef5 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 2 Feb 2023 17:53:07 +0000 Subject: [PATCH 32/35] [Fleet] Fix discard changes link taking user to "page not found" (#150174) The history blocker which we use to display the discard changes modal on the policy editor was not adding the base path to the URL. This is because state.pathname doesnt have the basePath infront of it, I'm not sure why! In the unit tests it does which makes it a bit odd. I also noticed we had duplicated the useHistoryBlock hook. Screenshot 2023-02-02 at 13 54 24 --- .../hooks/index.test.tsx | 151 ------------------ .../edit_package_policy_page/hooks/index.tsx | 45 +----- .../hooks/use_history_block.test.tsx | 8 +- .../hooks/use_history_block.tsx | 9 +- .../edit_package_policy_page/index.tsx | 4 +- 5 files changed, 12 insertions(+), 205 deletions(-) delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.test.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.test.tsx deleted file mode 100644 index daf8177b9e73..000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.test.tsx +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from '@testing-library/react-hooks'; - -import { createFleetTestRendererMock } from '../../../../../../mock'; - -import { useHistoryBlock } from '.'; - -describe('useHistoryBlock', () => { - describe('without search params', () => { - it('should not block if not edited', () => { - const renderer = createFleetTestRendererMock(); - - renderer.renderHook(() => useHistoryBlock(false)); - - act(() => renderer.mountHistory.push('/test')); - - const { location } = renderer.mountHistory; - expect(location.pathname).toBe('/test'); - expect(location.search).toBe(''); - expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); - }); - - it('should block if edited', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(true); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test', - expect.anything() - ); - }); - - it('should block if edited and not navigate on cancel', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(false); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); - }); - }); - describe('with search params', () => { - it('should not block if not edited', () => { - const renderer = createFleetTestRendererMock(); - - renderer.renderHook(() => useHistoryBlock(false)); - - act(() => renderer.mountHistory.push('/test?param=test')); - - const { location } = renderer.mountHistory; - expect(location.pathname).toBe('/test'); - expect(location.search).toBe('?param=test'); - expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); - }); - - it('should block if edited and navigate on confirm', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(true); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test?param=test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test?param=test', - expect.anything() - ); - }); - - it('should block if edited and not navigate on cancel', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(false); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test?param=test')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); - }); - }); - - describe('with hash params', () => { - it('should not block if not edited', () => { - const renderer = createFleetTestRendererMock(); - - renderer.renderHook(() => useHistoryBlock(false)); - - act(() => renderer.mountHistory.push('/test#/hash')); - - const { location } = renderer.mountHistory; - expect(location.pathname).toBe('/test'); - expect(location.hash).toBe('#/hash'); - expect(renderer.startServices.overlays.openConfirm).not.toBeCalled(); - }); - - it('should block if edited and navigate on confirm', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(true); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test#/hash')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test#/hash', - expect.anything() - ); - }); - - it('should block if edited and not navigate on cancel', async () => { - const renderer = createFleetTestRendererMock(); - - renderer.startServices.overlays.openConfirm.mockResolvedValue(false); - renderer.renderHook(() => useHistoryBlock(true)); - - act(() => renderer.mountHistory.push('/test#/hash')); - // needed because we have an async useEffect - await act(() => new Promise((resolve) => resolve())); - - expect(renderer.startServices.overlays.openConfirm).toBeCalled(); - expect(renderer.startServices.application.navigateToUrl).not.toBeCalled(); - }); - }); -}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx index edf04f8733ad..7b295c9f53d5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/index.tsx @@ -5,46 +5,5 @@ * 2.0. */ -import { useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; - -import { useStartServices } from '../../../../hooks'; - -export function useHistoryBlock(isEdited: boolean) { - const history = useHistory(); - const { overlays, application } = useStartServices(); - - useEffect(() => { - if (!isEdited) { - return; - } - - const unblock = history.block((state) => { - async function confirmAsync() { - const confirmRes = await overlays.openConfirm( - i18n.translate('xpack.fleet.editPackagePolicy.historyBlockDescription', { - defaultMessage: `Unsaved changes will be discarded. Are you sure you would like to continue?`, - }), - { - title: i18n.translate('xpack.fleet.editPackagePolicy.historyBlockTitle', { - defaultMessage: 'Discard Changes?', - }), - } - ); - - if (confirmRes) { - unblock(); - - application.navigateToUrl(state.pathname + state.hash + state.search, { - state: state.state, - }); - } - } - confirmAsync(); - return false; - }); - - return unblock; - }, [history, isEdited, overlays, application]); -} +export { useHistoryBlock } from './use_history_block'; +export { usePackagePolicyWithRelatedData } from './use_package_policy'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx index 91a4afbda62e..491c5f627694 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.test.tsx @@ -11,6 +11,8 @@ import { createFleetTestRendererMock } from '../../../../../../mock'; import { useHistoryBlock } from './use_history_block'; +// our test mountHistory prepends the basePath to URLs, however useHistory state doesnt have the basePath +// in production, so we have to prepend it to the state.pathname, this results in /mock/mock in the assertions describe('useHistoryBlock', () => { describe('without search params', () => { it('should not block if not edited', () => { @@ -38,7 +40,7 @@ describe('useHistoryBlock', () => { expect(renderer.startServices.overlays.openConfirm).toBeCalled(); expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test', + '/mock/mock/test', expect.anything() ); }); @@ -83,7 +85,7 @@ describe('useHistoryBlock', () => { expect(renderer.startServices.overlays.openConfirm).toBeCalled(); expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test?param=test', + '/mock/mock/test?param=test', expect.anything() ); }); @@ -129,7 +131,7 @@ describe('useHistoryBlock', () => { expect(renderer.startServices.overlays.openConfirm).toBeCalled(); expect(renderer.startServices.application.navigateToUrl).toBeCalledWith( - '/mock/test#/hash', + '/mock/mock/test#/hash', expect.anything() ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx index edf04f8733ad..abdd287c7577 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_history_block.tsx @@ -13,7 +13,7 @@ import { useStartServices } from '../../../../hooks'; export function useHistoryBlock(isEdited: boolean) { const history = useHistory(); - const { overlays, application } = useStartServices(); + const { overlays, application, http } = useStartServices(); useEffect(() => { if (!isEdited) { @@ -32,11 +32,10 @@ export function useHistoryBlock(isEdited: boolean) { }), } ); - if (confirmRes) { + const url = http.basePath.prepend(state.pathname) + state.hash + state.search; unblock(); - - application.navigateToUrl(state.pathname + state.hash + state.search, { + application.navigateToUrl(url, { state: state.state, }); } @@ -46,5 +45,5 @@ export function useHistoryBlock(isEdited: boolean) { }); return unblock; - }, [history, isEdited, overlays, application]); + }, [history, isEdited, overlays, application, http.basePath]); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index b066457b0aa2..8b87f64d7b6c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -53,9 +53,8 @@ import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; import { ExperimentalFeaturesService, pkgKeyFromPackageInfo } from '../../../services'; import { generateUpdatePackagePolicyDevToolsRequest } from '../services'; -import { useHistoryBlock } from './hooks'; import { UpgradeStatusCallout } from './components'; -import { usePackagePolicyWithRelatedData } from './hooks/use_package_policy'; +import { usePackagePolicyWithRelatedData, useHistoryBlock } from './hooks'; export const EditPackagePolicyPage = memo(() => { const { @@ -163,7 +162,6 @@ export const EditPackagePolicyForm = memo<{ } return '/'; }, [from, getHref, packageInfo, policyId]); - const successRedirectPath = useMemo(() => { if (packageInfo && policyId) { return from === 'package-edit' || from === 'upgrade-from-integrations-policy-list' From 3f4f1cb8c7b622043fe8769661416c3e86261696 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 2 Feb 2023 11:13:45 -0700 Subject: [PATCH 33/35] Adds deprecation notice to saved objects API docs (#150124) Fix https://github.com/elastic/kibana/issues/149988 --- docs/api/saved-objects.asciidoc | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index 610f18c38d62..8a598fec9c47 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -6,9 +6,16 @@ Manage {kib} saved objects, including dashboards, visualizations, and more. WARNING: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future {kib} versions. -NOTE: For managing {data-sources}, use the <>. - The following saved objects APIs are available: +* <> to retrieve sets of saved objects that you want to import into {kib} + +* <> to create sets of {kib} saved objects from a file created by the export API + +* <> to resolve errors from the import API + +* <> to rotate the encryption key for encrypted saved objects + +deprecated::[8.7.0,Use <> for managing data views] * <> to retrieve a single {kib} saved object by ID @@ -32,13 +39,10 @@ The following saved objects APIs are available: * <> to remove multiple {kib} saved objects -* <> to retrieve sets of saved objects that you want to import into {kib} - -* <> to create sets of {kib} saved objects from a file created by the export API - -* <> to resolve errors from the import API - -* <> to rotate the encryption key for encrypted saved objects +include::saved-objects/export.asciidoc[] +include::saved-objects/import.asciidoc[] +include::saved-objects/resolve_import_errors.asciidoc[] +include::saved-objects/rotate_encryption_key.asciidoc[] include::saved-objects/get.asciidoc[] include::saved-objects/bulk_get.asciidoc[] @@ -49,9 +53,6 @@ include::saved-objects/update.asciidoc[] include::saved-objects/bulk_update.asciidoc[] include::saved-objects/delete.asciidoc[] include::saved-objects/bulk_delete.asciidoc[] -include::saved-objects/export.asciidoc[] -include::saved-objects/import.asciidoc[] -include::saved-objects/resolve_import_errors.asciidoc[] include::saved-objects/resolve.asciidoc[] include::saved-objects/bulk_resolve.asciidoc[] -include::saved-objects/rotate_encryption_key.asciidoc[] + From eee58fa7f282a3f4dbd956a85e307ad7ac6d3843 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Thu, 2 Feb 2023 20:24:25 +0200 Subject: [PATCH 34/35] [Cloud Posture] CIS AWS - Cloud dashboard supports new findings fields (#150086) --- .../common/schemas/csp_finding.ts | 21 ++++++-- .../schemas/csp_rule_template_metadata.ts | 5 +- .../cloud_security_posture/common/types.ts | 11 ++-- .../public/common/api/use_stats_api.ts | 14 ++--- .../compliance_dashboard.tsx | 3 +- .../benchmarks_section.test.tsx | 6 +-- .../dashboard_sections/benchmarks_section.tsx | 44 +++++++++++----- .../cluster_details_box.tsx | 20 ++++--- .../dashboard_sections/summary_section.tsx | 7 +-- .../public/pages/compliance_dashboard/mock.ts | 25 +++++++-- .../public/test/fixtures/findings_fixture.ts | 2 + .../create_indices/benchmark_score_mapping.ts | 3 ++ .../lib/get_identifier_runtime_mapping.ts | 52 +++++++++++++++++++ .../collectors/accounts_stats_collector.ts | 44 +--------------- .../collectors/resources_stats_collector.ts | 2 +- .../compliance_dashboard.ts | 18 ++++--- .../compliance_dashboard/get_clusters.test.ts | 9 ++-- .../compliance_dashboard/get_clusters.ts | 31 ++++++----- .../server/tasks/findings_stats_task.ts | 7 +-- 19 files changed, 205 insertions(+), 119 deletions(-) create mode 100644 x-pack/plugins/cloud_security_posture/server/lib/get_identifier_runtime_mapping.ts diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts index 27690c714c0c..c95962dd0218 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_finding.ts @@ -12,11 +12,8 @@ import type { CspRuleTemplateMetadata } from './csp_rule_template_metadata'; export interface CspFinding { '@timestamp': string; cluster_id: string; - orchestrator?: { - cluster?: { - name?: string; - }; - }; + orchestrator?: CspFindingOrchestrator; + cloud?: CspFindingCloud; // only available on CSPM findings result: CspFindingResult; resource: CspFindingResource; rule: CspRuleTemplateMetadata; @@ -28,6 +25,20 @@ export interface CspFinding { }; } +interface CspFindingOrchestrator { + cluster?: { + name?: string; + }; +} + +interface CspFindingCloud { + provider: 'aws'; + account: { + name: string; + id: string; + }; +} + interface CspFindingResult { evaluation: 'passed' | 'failed'; expected?: Record; diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts index 567c763f5d84..b466ed7e70d2 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_metadata.ts @@ -5,6 +5,7 @@ * 2.0. */ import { schema as rt, TypeOf } from '@kbn/config-schema'; +import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../constants'; export const cspRuleTemplateMetadataSchemaV840 = rt.object({ audit: rt.string(), @@ -32,10 +33,12 @@ export const cspRuleTemplateMetadataSchemaV870 = rt.object({ audit: rt.string(), benchmark: rt.object({ name: rt.string(), + posture_type: rt.maybe( + rt.oneOf([rt.literal(CSPM_POLICY_TEMPLATE), rt.literal(KSPM_POLICY_TEMPLATE)]) + ), id: rt.string(), version: rt.string(), rule_number: rt.maybe(rt.string()), - posture_type: rt.maybe(rt.string()), }), default_value: rt.maybe(rt.string()), description: rt.string(), diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index a922d363d9db..4bf34b201333 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -6,8 +6,9 @@ */ import type { PackagePolicy, AgentPolicy } from '@kbn/fleet-plugin/common'; +import { CspFinding } from './schemas/csp_finding'; import { SUPPORTED_CLOUDBEAT_INPUTS, SUPPORTED_POLICY_TEMPLATES } from './constants'; -import type { CspRuleTemplateMetadata } from './schemas/csp_rule_template_metadata'; +import { CspRuleTemplateMetadata } from './schemas/csp_rule_template_metadata'; export type Evaluation = 'passed' | 'failed' | 'NA'; /** number between 1-100 */ @@ -34,10 +35,10 @@ export interface PostureTrend extends Stats { export interface Cluster { meta: { - clusterId: string; - clusterName?: string; - benchmarkName: string; - benchmarkId: BenchmarkId; + assetIdentifierId: string; + cloud: CspFinding['cloud']; + benchmark: CspFinding['rule']['benchmark']; + cluster: NonNullable['cluster']; lastUpdate: string; }; stats: Stats; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts index 14c48b7a1720..4225c6a67c0b 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts @@ -7,8 +7,12 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'; import { useKibana } from '../hooks/use_kibana'; -import { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types'; -import { STATS_ROUTE_PATH } from '../../../common/constants'; +import { ComplianceDashboardData, PosturePolicyTemplate } from '../../../common/types'; +import { + CSPM_POLICY_TEMPLATE, + KSPM_POLICY_TEMPLATE, + STATS_ROUTE_PATH, +} from '../../../common/constants'; // TODO: consolidate both hooks into one hook with a dynamic key const getCspmStatsKey = ['csp_cspm_dashboard_stats']; @@ -24,8 +28,7 @@ export const useCspmStatsApi = ( const { http } = useKibana().services; return useQuery( getCspmStatsKey, - // TODO: CIS AWS - remove casting and use actual policy template instead of benchmark_id - () => http.get(getStatsRoute('cis_aws' as PosturePolicyTemplate)), + () => http.get(getStatsRoute(CSPM_POLICY_TEMPLATE)), options ); }; @@ -36,8 +39,7 @@ export const useKspmStatsApi = ( const { http } = useKibana().services; return useQuery( getKspmStatsKey, - // TODO: CIS AWS - remove casting and use actual policy template - () => http.get(getStatsRoute('cis_k8s' as PosturePolicyTemplate)), + () => http.get(getStatsRoute(KSPM_POLICY_TEMPLATE)), options ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 18ad2d498a69..946d4cd2a53d 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -26,7 +26,7 @@ import { DASHBOARD_CONTAINER, KUBERNETES_DASHBOARD_CONTAINER, } from './test_subjects'; -import { useCspmStatsApi, useKspmStatsApi } from '../../common/api'; +import { useCspmStatsApi, useKspmStatsApi } from '../../common/api/use_stats_api'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { NoFindingsStates } from '../../components/no_findings_states'; import { SummarySection } from './dashboard_sections/summary_section'; @@ -302,7 +302,6 @@ export const ComplianceDashboard = () => {
    ', () => { const mockDashboardDataCopy = getMockDashboardData(); const clusterMockDataCopy = getClusterMockData(); clusterMockDataCopy.stats.postureScore = 50; - clusterMockDataCopy.meta.clusterId = '1'; + clusterMockDataCopy.meta.assetIdentifierId = '1'; const clusterMockDataCopy1 = getClusterMockData(); clusterMockDataCopy1.stats.postureScore = 95; - clusterMockDataCopy1.meta.clusterId = '2'; + clusterMockDataCopy1.meta.assetIdentifierId = '2'; const clusterMockDataCopy2 = getClusterMockData(); clusterMockDataCopy2.stats.postureScore = 45; - clusterMockDataCopy2.meta.clusterId = '3'; + clusterMockDataCopy2.meta.assetIdentifierId = '3'; mockDashboardDataCopy.clusters = [ clusterMockDataCopy, diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx index bef98ce3cbb8..ba3f16dad46c 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/benchmarks_section.tsx @@ -7,20 +7,25 @@ import React, { useMemo } from 'react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, useEuiTheme } from '@elastic/eui'; import type { EuiIconProps } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { CloudPostureScoreChart } from '../compliance_charts/cloud_posture_score_chart'; import type { + Cluster, ComplianceDashboardData, Evaluation, PosturePolicyTemplate, } from '../../../../common/types'; import { LOCAL_STORAGE_DASHBOARD_CLUSTER_SORT_KEY } from '../../../common/constants'; import { RisksTable } from '../compliance_charts/risks_table'; -import { KSPM_POLICY_TEMPLATE, RULE_FAILED } from '../../../../common/constants'; +import { + CSPM_POLICY_TEMPLATE, + KSPM_POLICY_TEMPLATE, + RULE_FAILED, +} from '../../../../common/constants'; import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; import { ClusterDetailsBox } from './cluster_details_box'; import { dashboardColumnsGrow, getPolicyTemplateQuery } from './summary_section'; @@ -31,6 +36,17 @@ import { const CLUSTER_DEFAULT_SORT_ORDER = 'asc'; +export const getClusterIdQuery = (cluster: Cluster) => { + if (cluster.meta.benchmark.posture_type === CSPM_POLICY_TEMPLATE) { + return { 'cloud.account.name': cluster.meta.cloud?.account.name }; + } + if (cluster.meta.benchmark.posture_type === 'kspm') { + return { cluster_id: cluster.meta.assetIdentifierId }; + } + + return {}; +}; + export const BenchmarksSection = ({ complianceData, dashboardType, @@ -50,25 +66,25 @@ export const BenchmarksSection = ({ const clusterSortingIcon: EuiIconProps['type'] = isClusterSortingAsc ? 'sortUp' : 'sortDown'; - const navToFindingsByClusterAndEvaluation = (clusterId: string, evaluation: Evaluation) => { + const navToFindingsByClusterAndEvaluation = (cluster: Cluster, evaluation: Evaluation) => { navToFindings({ ...getPolicyTemplateQuery(dashboardType), - cluster_id: clusterId, + ...getClusterIdQuery(cluster), 'result.evaluation': evaluation, }); }; - const navToFailedFindingsByClusterAndSection = (clusterId: string, ruleSection: string) => { + const navToFailedFindingsByClusterAndSection = (cluster: Cluster, ruleSection: string) => { navToFindings({ ...getPolicyTemplateQuery(dashboardType), - cluster_id: clusterId, + ...getClusterIdQuery(cluster), 'rule.section': ruleSection, 'result.evaluation': RULE_FAILED, }); }; - const navToFailedFindingsByCluster = (clusterId: string) => { - navToFindingsByClusterAndEvaluation(clusterId, RULE_FAILED); + const navToFailedFindingsByCluster = (cluster: Cluster) => { + navToFindingsByClusterAndEvaluation(cluster, RULE_FAILED); }; const toggleClustersSortingDirection = () => { @@ -144,8 +160,10 @@ export const BenchmarksSection = ({ {clusters.map((cluster) => ( - navToFindingsByClusterAndEvaluation(cluster.meta.clusterId, evaluation) + navToFindingsByClusterAndEvaluation(cluster, evaluation) } /> @@ -173,13 +191,13 @@ export const BenchmarksSection = ({ data={cluster.groupedFindingsEvaluation} maxItems={3} onCellClick={(resourceTypeName) => - navToFailedFindingsByClusterAndSection(cluster.meta.clusterId, resourceTypeName) + navToFailedFindingsByClusterAndSection(cluster, resourceTypeName) } viewAllButtonTitle={i18n.translate( 'xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle', { defaultMessage: 'View all failed findings for this cluster' } )} - onViewAllClick={() => navToFailedFindingsByCluster(cluster.meta.clusterId)} + onViewAllClick={() => navToFailedFindingsByCluster(cluster)} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx index bdb21f12883b..8e8bfc2ac094 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cluster_details_box.tsx @@ -19,6 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import moment from 'moment'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import { getClusterIdQuery } from './benchmarks_section'; import { INTERNAL_FEATURE_FLAGS } from '../../../../common/constants'; import { Cluster } from '../../../../common/types'; import { useNavigateFindings } from '../../../common/hooks/use_navigate_findings'; @@ -26,18 +27,23 @@ import { CISBenchmarkIcon } from '../../../components/cis_benchmark_icon'; const defaultClusterTitle = i18n.translate( 'xpack.csp.dashboard.benchmarkSection.defaultClusterTitle', - { defaultMessage: 'Cluster ID' } + { defaultMessage: 'ID' } ); +const getClusterTitle = (cluster: Cluster) => { + if (cluster.meta.benchmark.posture_type === 'cspm') return cluster.meta.cloud?.account.name; + if (cluster.meta.benchmark.posture_type === 'kspm') return cluster.meta.cluster?.name; +}; + export const ClusterDetailsBox = ({ cluster }: { cluster: Cluster }) => { const { euiTheme } = useEuiTheme(); const navToFindings = useNavigateFindings(); - const shortId = cluster.meta.clusterId.slice(0, 6); - const title = cluster.meta.clusterName || defaultClusterTitle; + const shortId = cluster.meta.assetIdentifierId.slice(0, 6); + const title = getClusterTitle(cluster) || defaultClusterTitle; - const handleClusterTitleClick = (clusterId: string) => { - navToFindings({ cluster_id: clusterId }); + const handleClusterTitleClick = () => { + return navToFindings(getClusterIdQuery(cluster)); }; return ( @@ -64,7 +70,7 @@ export const ClusterDetailsBox = ({ cluster }: { cluster: Cluster }) => { } > - handleClusterTitleClick(cluster.meta.clusterId)} color="text"> +
    { grow={true} style={{ justifyContent: 'flex-end', paddingBottom: euiTheme.size.m }} > - + {INTERNAL_FEATURE_FLAGS.showManageRulesMock && ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx index f7ef39cb00f9..7a4b3b3d7fd9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/summary_section.tsx @@ -36,10 +36,11 @@ export const dashboardColumnsGrow: Record = { third: 8, }; -// TODO: CIS AWS - replace query to use policy_template field when available export const getPolicyTemplateQuery = (policyTemplate: PosturePolicyTemplate) => { - if (policyTemplate === CSPM_POLICY_TEMPLATE) return { 'rule.benchmark.id': 'cis_aws' }; - if (policyTemplate === KSPM_POLICY_TEMPLATE) return { 'rule.benchmark.id': 'cis_k8s' }; + if (policyTemplate === CSPM_POLICY_TEMPLATE) + return { 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE }; + if (policyTemplate === KSPM_POLICY_TEMPLATE) + return { 'rule.benchmark.posture_type': KSPM_POLICY_TEMPLATE }; return {}; }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts index f81b52473338..19040892e7e6 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/mock.ts @@ -5,14 +5,29 @@ * 2.0. */ -import { ComplianceDashboardData } from '../../../common/types'; +import { Cluster, ComplianceDashboardData } from '../../../common/types'; -export const getClusterMockData = () => ({ +export const getClusterMockData = (): Cluster => ({ meta: { - clusterId: '8f9c5b98-cc02-4827-8c82-316e2cc25870', - benchmarkName: 'CIS Kubernetes V1.20', + assetIdentifierId: '8f9c5b98-cc02-4827-8c82-316e2cc25870', lastUpdate: '2022-11-07T13:14:34.990Z', - benchmarkId: 'cis_k8s', + cloud: { + provider: 'aws', + account: { + name: 'build-security-dev', + id: '704479110758', + }, + }, + benchmark: { + name: 'CIS Amazon Web Services Foundations', + rule_number: '1.4', + id: 'cis_aws', + posture_type: 'cspm', + version: 'v1.5.0', + }, + cluster: { + name: '8f9c5b98-cc02-4827-8c82-316e2cc25870', + }, }, stats: { totalFailed: 17, diff --git a/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts b/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts index f2c325787f9f..62921cd534e6 100644 --- a/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts +++ b/x-pack/plugins/cloud_security_posture/public/test/fixtures/findings_fixture.ts @@ -29,6 +29,8 @@ export const getFindingsFixture = (): CspFinding & { id: string } => ({ name: 'CIS Kubernetes', version: '1.6.0', id: 'cis_k8s', + rule_number: '1.1.1', + posture_type: 'kspm', }, default_value: chance.sentence(), description: chance.paragraph(), diff --git a/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts b/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts index 584b23652769..b1f3e64521a3 100644 --- a/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts +++ b/x-pack/plugins/cloud_security_posture/server/create_indices/benchmark_score_mapping.ts @@ -26,6 +26,9 @@ export const benchmarkScoreMapping: MappingTypeMapping = { cluster_id: { type: 'keyword', }, + 'cloud.account.id': { + type: 'keyword', + }, 'rule.benchmark.name': { type: 'keyword', }, diff --git a/x-pack/plugins/cloud_security_posture/server/lib/get_identifier_runtime_mapping.ts b/x-pack/plugins/cloud_security_posture/server/lib/get_identifier_runtime_mapping.ts new file mode 100644 index 000000000000..2e0bb0a04b5d --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/lib/get_identifier_runtime_mapping.ts @@ -0,0 +1,52 @@ +/* + * 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 { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; + +/** + * Creates the `asset_identifier` runtime field with the value of either + * `account.cloud.id` or `cluster.id` based on the value of `rule.benchmark.posture_type` + */ +export const getIdentifierRuntimeMapping = (): MappingRuntimeFields => ({ + asset_identifier: { + type: 'keyword', + script: { + source: ` + if (!doc.containsKey('rule.benchmark.posture_type')) + { + def identifier = doc["cluster_id"].value; + emit(identifier); + return + } + else + { + if(doc["rule.benchmark.posture_type"].size() > 0) + { + def policy_template_type = doc["rule.benchmark.posture_type"].value; + if (policy_template_type == "cspm") + { + def identifier = doc["cloud.account.id"].value; + emit(identifier); + return + } + + if (policy_template_type == "kspm") + { + def identifier = doc["cluster_id"].value; + emit(identifier); + return + } + } + + def identifier = doc["cluster_id"].value; + emit(identifier); + return + } + `, + }, + }, +}); diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts index 1f4f8aeaa659..1402b45f3d22 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/accounts_stats_collector.ts @@ -6,7 +6,8 @@ */ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { Logger } from '@kbn/core/server'; -import type { MappingRuntimeFields, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getIdentifierRuntimeMapping } from '../../get_identifier_runtime_mapping'; import { calculatePostureScore } from '../../../../common/utils/helpers'; import type { CspmAccountsStats } from './types'; import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants'; @@ -52,47 +53,6 @@ interface AccountEntity { }; } -// The runtime field help to have unique identifier for CSPM and KSPM -export const getIdentifierRuntimeMapping = (): MappingRuntimeFields => ({ - asset_identifier: { - type: 'keyword', - script: { - source: ` - if (!doc.containsKey('rule.benchmark.posture_type')) - { - def identifier = doc["cluster_id"].value; - emit(identifier); - return - } - else - { - if(doc["rule.benchmark.posture_type"].size() > 0) - { - def policy_template_type = doc["rule.benchmark.posture_type"].value; - if (policy_template_type == "cspm") - { - def identifier = doc["cloud.account.id"].value; - emit(identifier); - return - } - - if (policy_template_type == "kspm") - { - def identifier = doc["cluster_id"].value; - emit(identifier); - return - } - } - - def identifier = doc["cluster_id"].value; - emit(identifier); - return - } - `, - }, - }, -}); - const getAccountsStatsQuery = (index: string): SearchRequest => ({ index, runtime_mappings: getIdentifierRuntimeMapping(), diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts index 89250a55d16d..7308ab7e9159 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/resources_stats_collector.ts @@ -7,9 +7,9 @@ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import type { Logger } from '@kbn/core/server'; import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getIdentifierRuntimeMapping } from '../../get_identifier_runtime_mapping'; import type { CspmResourcesStats } from './types'; import { LATEST_FINDINGS_INDEX_DEFAULT_NS } from '../../../../common/constants'; -import { getIdentifierRuntimeMapping } from './accounts_stats_collector'; interface ResourcesStats { accounts: { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts index b59ad93d8f25..6789b8674499 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts @@ -9,7 +9,12 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { schema } from '@kbn/config-schema'; import type { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types'; -import { LATEST_FINDINGS_INDEX_DEFAULT_NS, STATS_ROUTE_PATH } from '../../../common/constants'; +import { + CSPM_POLICY_TEMPLATE, + KSPM_POLICY_TEMPLATE, + LATEST_FINDINGS_INDEX_DEFAULT_NS, + STATS_ROUTE_PATH, +} from '../../../common/constants'; import { getGroupedFindingsEvaluation } from './get_grouped_findings_evaluation'; import { ClusterWithoutTrend, getClusters } from './get_clusters'; import { getStats } from './get_stats'; @@ -26,7 +31,7 @@ const getClustersTrends = (clustersWithoutTrends: ClusterWithoutTrend[], trends: ...cluster, trend: trends.map(({ timestamp, clusters: clustersTrendData }) => ({ timestamp, - ...clustersTrendData[cluster.meta.clusterId], + ...clustersTrendData[cluster.meta.assetIdentifierId], })), })); @@ -35,8 +40,10 @@ const getSummaryTrend = (trends: Trends) => const queryParamsSchema = { params: schema.object({ - // TODO: CIS AWS - replace with strict policy template values once available - policy_template: schema.string(), + policy_template: schema.oneOf([ + schema.literal(CSPM_POLICY_TEMPLATE), + schema.literal(KSPM_POLICY_TEMPLATE), + ]), }), }; @@ -64,8 +71,7 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter): void => const query: QueryDslQueryContainer = { bool: { - // TODO: CIS AWS - replace filtered field to `policy_template` when available - filter: [{ term: { 'rule.benchmark.id': policyTemplate } }], + filter: [{ term: { 'rule.benchmark.posture_type': policyTemplate } }], }, }; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts index ba97be21ad71..78bd29910cd4 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.test.ts @@ -76,10 +76,13 @@ describe('getClustersFromAggs', () => { { meta: { lastUpdate: '123', - clusterName: 'cluster_name', clusterId: 'cluster_id', - benchmarkName: 'CIS Kubernetes', - benchmarkId: 'cis_k8s', + assetIdentifierId: 'cluster_id', + benchmark: { name: 'CIS Kubernetes', id: 'cis_k8s' }, + cloud: undefined, + cluster: { + name: 'cluster_name', + }, }, stats: { totalFindings: 12, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts index 388672f25409..eb1f21efed65 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_clusters.ts @@ -22,6 +22,7 @@ import { import type { FailedFindingsQueryResult } from './get_grouped_findings_evaluation'; import { findingsEvaluationAggsQuery, getStatsFromFindingsEvaluationsAggs } from './get_stats'; import { KeyDocCount } from './compliance_dashboard'; +import { getIdentifierRuntimeMapping } from '../../lib/get_identifier_runtime_mapping'; export interface ClusterBucket extends FailedFindingsQueryResult, KeyDocCount { failed_findings: { @@ -34,18 +35,19 @@ export interface ClusterBucket extends FailedFindingsQueryResult, KeyDocCount { } interface ClustersQueryResult { - aggs_by_cluster_id: Aggregation; + aggs_by_asset_identifier: Aggregation; } export type ClusterWithoutTrend = Omit; export const getClustersQuery = (query: QueryDslQueryContainer, pitId: string): SearchRequest => ({ size: 0, + runtime_mappings: getIdentifierRuntimeMapping(), query, aggs: { - aggs_by_cluster_id: { + aggs_by_asset_identifier: { terms: { - field: 'cluster_id', + field: 'asset_identifier', }, aggs: { latestFindingTopHit: { @@ -65,25 +67,26 @@ export const getClustersQuery = (query: QueryDslQueryContainer, pitId: string): }); export const getClustersFromAggs = (clusters: ClusterBucket[]): ClusterWithoutTrend[] => - clusters.map((cluster) => { - const latestFindingHit: SearchHit = cluster.latestFindingTopHit.hits.hits[0]; + clusters.map((clusterBucket) => { + const latestFindingHit: SearchHit = clusterBucket.latestFindingTopHit.hits.hits[0]; if (!latestFindingHit._source) throw new Error('Missing findings top hits'); const meta = { - clusterId: cluster.key, - clusterName: latestFindingHit._source.orchestrator?.cluster?.name, - benchmarkName: latestFindingHit._source.rule.benchmark.name, - benchmarkId: latestFindingHit._source.rule.benchmark.id, + clusterId: clusterBucket.key, + assetIdentifierId: clusterBucket.key, lastUpdate: latestFindingHit._source['@timestamp'], + benchmark: latestFindingHit._source.rule.benchmark, + cloud: latestFindingHit._source.cloud, // only available on CSPM findings + cluster: latestFindingHit._source.orchestrator?.cluster, // only available on KSPM findings }; // get cluster's stats - if (!cluster.failed_findings || !cluster.passed_findings) - throw new Error('missing findings evaluations per cluster'); - const stats = getStatsFromFindingsEvaluationsAggs(cluster); + if (!clusterBucket.failed_findings || !clusterBucket.passed_findings) + throw new Error('missing findings evaluations per cluster bucket'); + const stats = getStatsFromFindingsEvaluationsAggs(clusterBucket); // get cluster's resource types aggs - const resourcesTypesAggs = cluster.aggs_by_resource_type.buckets; + const resourcesTypesAggs = clusterBucket.aggs_by_resource_type.buckets; if (!Array.isArray(resourcesTypesAggs)) throw new Error('missing aggs by resource type per cluster'); const groupedFindingsEvaluation = getFailedFindingsFromAggs(resourcesTypesAggs); @@ -104,7 +107,7 @@ export const getClusters = async ( getClustersQuery(query, pitId) ); - const clusters = queryResult.aggregations?.aggs_by_cluster_id.buckets; + const clusters = queryResult.aggregations?.aggs_by_asset_identifier.buckets; if (!Array.isArray(clusters)) throw new Error('missing aggs by cluster id'); return getClustersFromAggs(clusters); diff --git a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts index 96a410d3cdae..2895fb43d0b5 100644 --- a/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts +++ b/x-pack/plugins/cloud_security_posture/server/tasks/findings_stats_task.ts @@ -14,6 +14,7 @@ import { import { SearchRequest } from '@kbn/data-plugin/common'; import { ElasticsearchClient } from '@kbn/core/server'; import type { Logger } from '@kbn/core/server'; +import { getIdentifierRuntimeMapping } from '../lib/get_identifier_runtime_mapping'; import { FindingsStatsTaskResult, TaskHealthStatus, ScoreByPolicyTemplateBucket } from './types'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS, @@ -107,14 +108,14 @@ export function taskRunner(coreStartServices: CspServerPluginStartServices, logg const getScoreQuery = (): SearchRequest => ({ index: LATEST_FINDINGS_INDEX_DEFAULT_NS, size: 0, + runtime_mappings: getIdentifierRuntimeMapping(), query: { match_all: {}, }, aggs: { score_by_policy_template: { terms: { - // TODO: CIS AWS - replace with policy_template when available - field: 'rule.benchmark.id', + field: 'rule.benchmark.posture_type', }, aggs: { total_findings: { @@ -138,7 +139,7 @@ const getScoreQuery = (): SearchRequest => ({ }, score_by_cluster_id: { terms: { - field: 'cluster_id', + field: 'asset_identifier', }, aggregations: { total_findings: { From 5465ac725ce5f7f14aa10538538dd3498c657698 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Thu, 2 Feb 2023 19:33:31 +0100 Subject: [PATCH 35/35] [Enterprise Search] Fix bug with pagination on fetch sync jobs (#150206) ## Summary Fixes an API validation bug I missed when updating pagination. --- .../server/routes/enterprise_search/connectors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts index 382b67d7a32f..8861722205ea 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -159,7 +159,7 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { connectorId: schema.string(), }), query: schema.object({ - page: schema.number({ defaultValue: 0, min: 0 }), + from: schema.number({ defaultValue: 0, min: 0 }), size: schema.number({ defaultValue: 10, min: 0 }), }), }, @@ -169,7 +169,7 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { const result = await fetchSyncJobsByConnectorId( client, request.params.connectorId, - request.query.page, + request.query.from, request.query.size ); return response.ok({ body: result });