diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 908d0c9c784f7..78f3271d028a9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -608,20 +608,19 @@ /x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions @elastic/security-solution-platform @elastic/security-detections-response-rules /x-pack/plugins/security_solution/server/routes @elastic/security-detections-response @elastic/security-threat-hunting - -## Security Solution sub teams - security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/public/management/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/public/common/lib/endpoint*/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/common/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/server/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/server/lists_integration/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/server/lib/license/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/server/fleet_integration/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/scripts/endpoint/event_filters/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/test/security_solution_endpoint/apps/endpoint/ @elastic/security-onboarding-and-lifecycle-mgt -/x-pack/test/security_solution_endpoint_api_int/ @elastic/security-onboarding-and-lifecycle-mgt +## Security Solution sub teams - security-defend-workflows +/x-pack/plugins/security_solution/public/management/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/common/lib/endpoint*/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/common/components/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/common/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/lists_integration/endpoint/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/lib/license/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/fleet_integration/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/scripts/endpoint/event_filters/ @elastic/security-defend-workflows +/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-defend-workflows +/x-pack/test/security_solution_endpoint/apps/endpoint/ @elastic/security-defend-workflows +/x-pack/test/security_solution_endpoint_api_int/ @elastic/security-defend-workflows ## Security Solution sub teams - security-telemetry (Data Engineering) x-pack/plugins/security_solution/server/usage/ @elastic/security-data-analytics @@ -644,11 +643,11 @@ x-pack/plugins/threat_intelligence @elastic/protections-experience x-pack/plugins/security_solution/public/threat_intelligence @elastic/protections-experience x-pack/test/threat_intelligence_cypress @elastic/protections-experience -# Security Asset Management -/x-pack/plugins/osquery @elastic/security-asset-management -/x-pack/plugins/security_solution/common/detection_engine/rule_response_actions @elastic/security-asset-management -/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions @elastic/security-asset-management -/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions @elastic/security-asset-management +# Security Defend Workflows - OSQuery Ownership +/x-pack/plugins/osquery @elastic/security-defend-workflows +/x-pack/plugins/security_solution/common/detection_engine/rule_response_actions @elastic/security-defend-workflows +/x-pack/plugins/security_solution/public/detection_engine/rule_response_actions @elastic/security-defend-workflows +/x-pack/plugins/security_solution/server/lib/detection_engine/rule_response_actions @elastic/security-defend-workflows # Cloud Security Posture /x-pack/plugins/cloud_security_posture/ @elastic/kibana-cloud-security-posture diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 6d907ba23a807..41319a85612ee 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -197,3 +197,6 @@ xpack.fleet.agentPolicies: - type: winlog enabled: false ---- + +`xpack.fleet.enableExperimental`:: +List of experimental feature flag to enable in Fleet. \ No newline at end of file diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index 1d8c61a6e9a07..2dd6058106a56 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -50,7 +50,7 @@ endif::[] .macOS Gatekeeper warnings ==== Apple's rollout of stricter notarization requirements affected the notarization -of the {version} {kib} artifacts. If macOS Catalina displays a dialog when you +of the {version} {kib} artifacts. If macOS displays a dialog when you first run {kib} that interrupts it, you will need to take an action to allow it to run. diff --git a/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx b/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx index f7ebcbc42b88b..2af90e9be5edb 100644 --- a/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx +++ b/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx @@ -95,16 +95,18 @@ export const GettingStarted = () => { } }, [cloud, history]); + useEffect(() => { + // disable welcome screen on the home page + localStorage.setItem(KEY_ENABLE_WELCOME, JSON.stringify(false)); + }, []); + const onSkip = async () => { try { await guidedOnboardingService?.skipGuidedOnboarding(); } catch (error) { // if the state update fails, it's safe to ignore the error } - trackUiMetric(METRIC_TYPE.CLICK, 'guided_onboarding__skipped'); - // disable welcome screen on the home page - localStorage.setItem(KEY_ENABLE_WELCOME, JSON.stringify(false)); application.navigateToApp('home'); }; const { euiTheme } = useEuiTheme(); diff --git a/src/plugins/kibana_react/public/markdown/_markdown.scss b/src/plugins/kibana_react/public/markdown/_markdown.scss index 3c9b1cd165bab..c11aefe1f4d97 100644 --- a/src/plugins/kibana_react/public/markdown/_markdown.scss +++ b/src/plugins/kibana_react/public/markdown/_markdown.scss @@ -143,6 +143,7 @@ $kbnDefaultFontSize: 14px; max-width: 100%; box-sizing: content-box; border-style: none; + pointer-events: auto; } // 4. Blockquotes diff --git a/test/functional/apps/home/_breadcrumbs.ts b/test/functional/apps/home/_breadcrumbs.ts index 1463e828ea0c5..06784fdec237d 100644 --- a/test/functional/apps/home/_breadcrumbs.ts +++ b/test/functional/apps/home/_breadcrumbs.ts @@ -24,23 +24,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(breadcrumb).to.be('Home'); }); - it('Getting started page should render breadcrumbs', async () => { - const isCloud = await deployment.isCloud(); - - if (isCloud) { - await PageObjects.common.navigateToUrl('home', '/getting_started', { - useActualUrl: true, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - - const firstBreadcrumb = await testSubjects.getVisibleText('breadcrumb first'); - const lastBreadcrumb = await testSubjects.getVisibleText('breadcrumb last'); - - expect(firstBreadcrumb).to.be('Home'); - expect(lastBreadcrumb).to.be('Setup guides'); - } - }); - it('Tutorials directory page should render breadcrumbs', async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, @@ -68,5 +51,40 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(firstBreadcrumb).to.be('Integrations'); expect(lastBreadcrumb).to.be(tutorialId.toUpperCase()); }); + + // The getting started page is only rendered on cloud, and therefore the tests are only run on cloud + describe('Getting started page', () => { + let isCloud: boolean; + + before(async () => { + isCloud = await deployment.isCloud(); + }); + + beforeEach(async () => { + if (isCloud) { + await PageObjects.common.navigateToUrl('home', '/getting_started', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + }); + + it('Getting started page should render breadcrumbs', async () => { + if (isCloud) { + const firstBreadcrumb = await testSubjects.getVisibleText('breadcrumb first'); + const lastBreadcrumb = await testSubjects.getVisibleText('breadcrumb last'); + + expect(firstBreadcrumb).to.be('Home'); + expect(lastBreadcrumb).to.be('Setup guides'); + } + }); + + it('Home page breadcrumb should navigate to home', async () => { + if (isCloud) { + await PageObjects.home.clickHomeBreadcrumb(); + expect(await PageObjects.home.isHomePageDisplayed()).to.be(true); + } + }); + }); }); } diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index e3e14645f2b9a..a6a5e2e6bc9ea 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -60,6 +60,10 @@ export class HomePageObject extends FtrService { return await this.testSubjects.isDisplayed('onboarding--landing-page'); } + async isHomePageDisplayed() { + return await this.testSubjects.isDisplayed('homeApp'); + } + async getVisibileSolutions() { const solutionPanels = await this.testSubjects.findAll('~homSolutionPanel', 2000); const panelAttributes = await Promise.all( @@ -212,6 +216,10 @@ export class HomePageObject extends FtrService { await this.testSubjects.click('homeLink'); } + async clickHomeBreadcrumb() { + await this.testSubjects.click('breadcrumb first'); + } + // open global nav if it's closed async openCollapsibleNav() { if (!(await this.testSubjects.exists('collapsibleNav'))) { diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 797b29ad2be32..794ec8b912294 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -6,7 +6,7 @@ */ export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status'; -export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats'; +export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats/{policy_template}'; export const BENCHMARKS_ROUTE_PATH = '/internal/cloud_security_posture/benchmarks'; export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture'; diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index 757ec5ebb0eb5..797cb5ddc86c4 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -76,6 +76,7 @@ interface BaseCspSetupStatus { installedPackagePolicies: number; healthyAgents: number; isPluginInitialized: boolean; + installedPolicyTemplates: PosturePolicyTemplate[]; } interface CspSetupNotInstalledStatus extends BaseCspSetupStatus { diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts index 37575e4adda42..d128713e6fdd6 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts @@ -64,7 +64,8 @@ const getInputType = (inputType: string): string => { // Get the last part of the input type, input type structure: cloudbeat/ return inputType.split('/')[1]; }; -export const getCSPKuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`; + +export const CSP_FLEET_PACKAGE_KUERY = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`; export function assert(condition: any, msg?: string): asserts condition { if (!condition) { diff --git a/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_eks_logo.svg b/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_eks_logo.svg index e31c56edc8f08..f892a500e5ee3 100644 --- a/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_eks_logo.svg +++ b/x-pack/plugins/cloud_security_posture/public/assets/icons/cis_eks_logo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/x-pack/plugins/cloud_security_posture/public/assets/illustrations/no_data_illustration.svg b/x-pack/plugins/cloud_security_posture/public/assets/illustrations/no_data_illustration.svg new file mode 100644 index 0000000000000..025262d2513b0 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/assets/illustrations/no_data_illustration.svg @@ -0,0 +1 @@ + diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/index.ts b/x-pack/plugins/cloud_security_posture/public/common/api/index.ts index 2468e095ff5f9..fb3caf4fa9814 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/index.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './use_compliance_dashboard_data_api'; +export * from './use_stats_api'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_compliance_dashboard_data_api.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_compliance_dashboard_data_api.ts deleted file mode 100644 index c1b108bb5f98a..0000000000000 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_compliance_dashboard_data_api.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { type QueryObserverOptions, useQuery } from '@tanstack/react-query'; -import { useKibana } from '../hooks/use_kibana'; -import { ComplianceDashboardData } from '../../../common/types'; -import { STATS_ROUTE_PATH } from '../../../common/constants'; - -const getStatsKey = ['csp_dashboard_stats']; - -export const useComplianceDashboardDataApi = ( - options: QueryObserverOptions -) => { - const { http } = useKibana().services; - return useQuery(getStatsKey, () => http.get(STATS_ROUTE_PATH), options); -}; 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 new file mode 100644 index 0000000000000..14c48b7a17208 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_stats_api.ts @@ -0,0 +1,43 @@ +/* + * 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 { 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'; + +// TODO: consolidate both hooks into one hook with a dynamic key +const getCspmStatsKey = ['csp_cspm_dashboard_stats']; +const getKspmStatsKey = ['csp_kspm_dashboard_stats']; + +export const getStatsRoute = (policyTemplate: PosturePolicyTemplate) => { + return STATS_ROUTE_PATH.replace('{policy_template}', policyTemplate); +}; + +export const useCspmStatsApi = ( + options: UseQueryOptions +) => { + 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)), + options + ); +}; + +export const useKspmStatsApi = ( + options: UseQueryOptions +) => { + const { http } = useKibana().services; + return useQuery( + getKspmStatsKey, + // TODO: CIS AWS - remove casting and use actual policy template + () => http.get(getStatsRoute('cis_k8s' as PosturePolicyTemplate)), + options + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/use_navigate_to_cis_integration.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_integration_link.ts similarity index 80% rename from x-pack/plugins/cloud_security_posture/public/common/navigation/use_navigate_to_cis_integration.ts rename to x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_integration_link.ts index a19be2f8fd629..8d6e0f6c38583 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/navigation/use_navigate_to_cis_integration.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/use_csp_integration_link.ts @@ -6,11 +6,13 @@ */ import { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public'; -import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../../../common/constants'; +import type { PosturePolicyTemplate } from '../../../common/types'; import { useCisKubernetesIntegration } from '../api/use_cis_kubernetes_integration'; import { useKibana } from '../hooks/use_kibana'; -export const useCISIntegrationLink = (): string | undefined => { +export const useCspIntegrationLink = ( + policyTemplate: PosturePolicyTemplate +): string | undefined => { const { http } = useKibana().services; const cisIntegration = useCisKubernetesIntegration(); @@ -18,7 +20,7 @@ export const useCISIntegrationLink = (): string | undefined => { const path = pagePathGetters .add_integration_to_policy({ - integration: CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + integration: policyTemplate, pkgkey: pkgKeyFromPackageInfo({ name: cisIntegration.data.item.name, version: cisIntegration.data.item.version, diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx index 5eba5fabe51df..749aa1ccb038a 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.test.tsx @@ -24,12 +24,13 @@ import { UseQueryResult } from '@tanstack/react-query'; import { CloudPosturePage } from './cloud_posture_page'; import { NoDataPage } from '@kbn/kibana-react-plugin/public'; import { useCspSetupStatusApi } from '../common/api/use_setup_status_api'; -import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration'; +import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link'; const chance = new Chance(); + jest.mock('../common/api/use_setup_status_api'); -jest.mock('../common/navigation/use_navigate_to_cis_integration'); jest.mock('../common/hooks/use_subscription_status'); +jest.mock('../common/navigation/use_csp_integration_link'); describe('', () => { beforeEach(() => { @@ -146,7 +147,7 @@ describe('', () => { data: { status: 'not-installed' }, }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); const children = chance.sentence(); renderCloudPosturePage({ children }); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx index da4038cc18854..027112add1517 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_posture_page.tsx @@ -7,20 +7,32 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import type { UseQueryResult } from '@tanstack/react-query'; -import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { + EuiButton, + EuiEmptyPrompt, + EuiImage, + EuiFlexGroup, + EuiFlexItem, + EuiLink, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { NoDataPage } from '@kbn/kibana-react-plugin/public'; +import { NoDataPage, NoDataPageProps } from '@kbn/kibana-react-plugin/public'; import { css } from '@emotion/react'; +import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../../common/constants'; import { SubscriptionNotAllowed } from './subscription_not_allowed'; import { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; import { FullSizeCenteredPage } from './full_size_centered_page'; import { useCspSetupStatusApi } from '../common/api/use_setup_status_api'; import { CspLoadingState } from './csp_loading_state'; -import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_integration'; +import { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link'; + +import noDataIllustration from '../assets/illustrations/no_data_illustration.svg'; export const LOADING_STATE_TEST_SUBJECT = 'cloud_posture_page_loading'; export const ERROR_STATE_TEST_SUBJECT = 'cloud_posture_page_error'; export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_package_not_installed'; +export const CSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_cspm_not_installed'; +export const KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_kspm_not_installed'; export const DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_posture_page_no_data'; export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed'; @@ -45,49 +57,110 @@ export const isCommonError = (error: unknown): error is CommonError => { return true; }; -const packageNotInstalledRenderer = (cisIntegrationLink?: string) => ( - - ( + +); + +const packageNotInstalledRenderer = ({ + kspmIntegrationLink, + cspmIntegrationLink, +}: { + kspmIntegrationLink?: string; + cspmIntegrationLink?: string; +}) => { + return ( + + } + title={ +

+

+ } + layout="horizontal" + color="plain" + body={ +

+ + learnMore: ( + // TODO: CIS AWS - replace link with general doc for both integartions + ), }} /> - ), - }, - }} - /> - -); +

+ } + actions={ + + + + + + + + + + + + + } + /> +
+ ); +}; const defaultLoadingRenderer = () => ( @@ -172,7 +245,8 @@ export const CloudPosturePage = ({ }: CloudPosturePageProps) => { const subscriptionStatus = useSubscriptionStatus(); const getSetupStatus = useCspSetupStatusApi(); - const cisIntegrationLink = useCISIntegrationLink(); + const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); + const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); const render = () => { if (subscriptionStatus.isError) { @@ -196,7 +270,7 @@ export const CloudPosturePage = ({ } if (getSetupStatus.data.status === 'not-installed') { - return packageNotInstalledRenderer(cisIntegrationLink); + return packageNotInstalledRenderer({ kspmIntegrationLink, cspmIntegrationLink }); } if (!query) { diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts index dde25b7477543..afbd7ffac9bf8 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/utils.ts @@ -19,7 +19,7 @@ import { SUPPORTED_POLICY_TEMPLATES, SUPPORTED_CLOUDBEAT_INPUTS, } from '../../../common/constants'; -import { type PostureInput, type PosturePolicyTemplate } from '../../../common/types'; +import type { PostureInput, PosturePolicyTemplate } from '../../../common/types'; import { assert } from '../../../common/utils/helpers'; import { cloudPostureIntegrations } from '../../common/constants'; diff --git a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts index 4f154805aae05..95f3a83de9708 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/components/test_subjects.ts @@ -17,4 +17,5 @@ export const NO_FINDINGS_STATUS_TEST_SUBJ = { INDEXING: 'status-api-indexing', INDEX_TIMEOUT: 'status-api-index-timeout', UNPRIVILEGED: 'status-api-unprivileged', + NO_FINDINGS: 'no-findings-found', }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx index f9172a93457dc..1d8b3d6e55a91 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.test.tsx @@ -16,12 +16,13 @@ import * as TEST_SUBJ from './test_subjects'; import { useCspBenchmarkIntegrations } from './use_csp_benchmark_integrations'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; -import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration'; +import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; jest.mock('./use_csp_benchmark_integrations'); jest.mock('../../common/api/use_setup_status_api'); jest.mock('../../common/hooks/use_subscription_status'); -jest.mock('../../common/navigation/use_navigate_to_cis_integration'); +jest.mock('../../common/navigation/use_csp_integration_link'); + const chance = new Chance(); describe('', () => { @@ -41,7 +42,7 @@ describe('', () => { }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); }); const renderBenchmarks = ( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx index 29bc94dd739ec..b013f6b33dac5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx @@ -7,22 +7,23 @@ import React, { useState } from 'react'; import { + EuiButton, EuiFieldSearch, EuiFieldSearchProps, - EuiButton, - EuiSpacer, EuiFlexGroup, EuiFlexItem, - EuiTextColor, - EuiText, EuiPageHeader, + EuiSpacer, + EuiText, + EuiTextColor, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; +import { KSPM_POLICY_TEMPLATE } from '../../../common/constants'; +import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; import { CloudPosturePageTitle } from '../../components/cloud_posture_page_title'; import { CloudPosturePage } from '../../components/cloud_posture_page'; -import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration'; import { BenchmarksTable } from './benchmarks_table'; import { useCspBenchmarkIntegrations, @@ -35,8 +36,9 @@ import { usePageSize } from '../../common/hooks/use_page_size'; const SEARCH_DEBOUNCE_MS = 300; +// TODO: CIS AWS - add cspm integration button as well const AddCisIntegrationButton = () => { - const cisIntegrationLink = useCISIntegrationLink(); + const cisIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); return ( - + diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx index d7a03e0cb679c..6aace059a5cc4 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.test.tsx @@ -13,20 +13,25 @@ import { TestProvider } from '../../test/test_provider'; import { ComplianceDashboard } from '.'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; -import { useComplianceDashboardDataApi } from '../../common/api/use_compliance_dashboard_data_api'; -import { DASHBOARD_CONTAINER } from './test_subjects'; +import { useKspmStatsApi, useCspmStatsApi } from '../../common/api/use_stats_api'; +import { + CLOUD_DASHBOARD_CONTAINER, + DASHBOARD_CONTAINER, + KUBERNETES_DASHBOARD_CONTAINER, +} from './test_subjects'; import { createReactQueryResponse } from '../../test/fixtures/react_query'; import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects'; import { useCISIntegrationPoliciesLink } from '../../common/navigation/use_navigate_to_cis_integration_policies'; -import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration'; +import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; import { expectIdsInDoc } from '../../test/utils'; import { ComplianceDashboardData } from '../../../common/types'; jest.mock('../../common/api/use_setup_status_api'); -jest.mock('../../common/api/use_compliance_dashboard_data_api'); +jest.mock('../../common/api/use_stats_api'); jest.mock('../../common/hooks/use_subscription_status'); jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies'); -jest.mock('../../common/navigation/use_navigate_to_cis_integration'); +jest.mock('../../common/navigation/use_csp_integration_link'); + const chance = new Chance(); export const mockDashboardData: ComplianceDashboardData = { @@ -205,6 +210,17 @@ describe('', () => { data: true, }) ); + + (useCspmStatsApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + }) + ); }); const renderComplianceDashboardPage = () => { @@ -233,11 +249,11 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'not-deployed' }, + data: { status: 'not-deployed', installedPolicyTemplates: [] }, }) ); (useCISIntegrationPoliciesLink as jest.Mock).mockImplementation(() => chance.url()); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderComplianceDashboardPage(); @@ -256,10 +272,10 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'indexing' }, + data: { status: 'indexing', installedPolicyTemplates: [] }, }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderComplianceDashboardPage(); @@ -278,10 +294,10 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'index-timeout' }, + data: { status: 'index-timeout', installedPolicyTemplates: [] }, }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderComplianceDashboardPage(); @@ -300,10 +316,10 @@ describe('', () => { (useCspSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', - data: { status: 'unprivileged' }, + data: { status: 'unprivileged', installedPolicyTemplates: [] }, }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderComplianceDashboardPage(); @@ -319,7 +335,18 @@ describe('', () => { }); it('shows dashboard when there are findings in latest findings index', () => { - (useComplianceDashboardDataApi as jest.Mock).mockImplementation(() => ({ + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: mockDashboardData, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ isSuccess: true, isLoading: false, data: mockDashboardData, @@ -337,4 +364,196 @@ describe('', () => { ], }); }); + + it('Show Kubernetes dashboard if there are KSPM findings', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: mockDashboardData, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: undefined, + })); + + renderComplianceDashboardPage(); + + expectIdsInDoc({ + be: [KUBERNETES_DASHBOARD_CONTAINER], + notToBe: [ + CLOUD_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('Show Cloud dashboard if there are CSPM findings', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: undefined, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: mockDashboardData, + })); + + renderComplianceDashboardPage(); + + expectIdsInDoc({ + be: [CLOUD_DASHBOARD_CONTAINER], + notToBe: [ + KUBERNETES_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('Show Cloud dashboard "no findings prompt" if the CSPM integration is installed without findings', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed', installedPolicyTemplates: ['cspm'] }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + + renderComplianceDashboardPage(); + + expectIdsInDoc({ + be: [CLOUD_DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_FINDINGS], + notToBe: [ + KUBERNETES_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('Show Kubernetes dashboard "no findings prompt" if the KSPM integration is installed without findings', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed', installedPolicyTemplates: ['kspm'] }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + + renderComplianceDashboardPage(); + + expectIdsInDoc({ + be: [KUBERNETES_DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_FINDINGS], + notToBe: [ + CLOUD_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('Prefer Cloud dashboard if both integration are installed', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: { stats: { totalFindings: 0 } }, + })); + + renderComplianceDashboardPage(); + + expectIdsInDoc({ + be: [CLOUD_DASHBOARD_CONTAINER, NO_FINDINGS_STATUS_TEST_SUBJ.NO_FINDINGS], + notToBe: [ + KUBERNETES_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); + + it('Prefer Cloud dashboard if both integration have findings', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed', installedPolicyTemplates: ['cspm', 'kspm'] }, + }) + ); + (useKspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: mockDashboardData, + })); + (useCspmStatsApi as jest.Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: mockDashboardData, + })); + + renderComplianceDashboardPage(); + + expectIdsInDoc({ + be: [CLOUD_DASHBOARD_CONTAINER], + notToBe: [ + KUBERNETES_DASHBOARD_CONTAINER, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEX_TIMEOUT, + NO_FINDINGS_STATUS_TEST_SUBJ.NO_AGENTS_DEPLOYED, + NO_FINDINGS_STATUS_TEST_SUBJ.INDEXING, + NO_FINDINGS_STATUS_TEST_SUBJ.UNPRIVILEGED, + ], + }); + }); }); 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 33fa8756c631d..78ee59684896c 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 @@ -5,30 +5,284 @@ * 2.0. */ -import React from 'react'; -import { EuiSpacer, EuiPageHeader } from '@elastic/eui'; +import React, { useEffect, useMemo, useState } from 'react'; +import { EuiEmptyPrompt, EuiIcon, EuiLink, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { CloudSummarySection } from './dashboard_sections/cloud_summary_section'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects'; +import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; +import type { PosturePolicyTemplate, ComplianceDashboardData } from '../../../common/types'; import { CloudPosturePageTitle } from '../../components/cloud_posture_page_title'; -import { CloudPosturePage } from '../../components/cloud_posture_page'; -import { DASHBOARD_CONTAINER } from './test_subjects'; -import { useComplianceDashboardDataApi } from '../../common/api'; +import { + CloudPosturePage, + CspNoDataPage, + CspNoDataPageProps, + KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT, + CSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT, +} from '../../components/cloud_posture_page'; +import { + CLOUD_DASHBOARD_CONTAINER, + DASHBOARD_CONTAINER, + KUBERNETES_DASHBOARD_CONTAINER, +} from './test_subjects'; +import { useCspmStatsApi, useKspmStatsApi } from '../../common/api'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { NoFindingsStates } from '../../components/no_findings_states'; +import { CloudSummarySection } from './dashboard_sections/cloud_summary_section'; import { CloudBenchmarksSection } from './dashboard_sections/cloud_benchmarks_section'; +import { CSPM_POLICY_TEMPLATE, KSPM_POLICY_TEMPLATE } from '../../../common/constants'; + +const noDataOptions: Record< + PosturePolicyTemplate, + Pick & { testId: string } +> = { + kspm: { + testId: KSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT, + docsLink: 'https://ela.st/kspm', + actionTitle: i18n.translate( + 'xpack.csp.cloudPosturePage.kspmIntegration.packageNotInstalled.buttonLabel', + { defaultMessage: 'Add a KSPM integration' } + ), + actionDescription: ( + + + + ), + }} + /> + ), + }, + cspm: { + testId: CSPM_INTEGRATION_NOT_INSTALLED_TEST_SUBJECT, + // TODO: CIS AWS - replace link or create the docs + docsLink: 'https://ela.st/cspm', + actionTitle: i18n.translate( + 'xpack.csp.cloudPosturePage.cspmIntegration.packageNotInstalled.buttonLabel', + { defaultMessage: 'Add a CSPM integration' } + ), + actionDescription: ( + + + + ), + }} + /> + ), + }, +}; + +const getNotInstalledConfig = ( + policyTemplate: PosturePolicyTemplate, + actionHref: CspNoDataPageProps['actionHref'] +) => { + const policyTemplateNoDataConfig = noDataOptions[policyTemplate]; + + return { + pageTitle: i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle', { + defaultMessage: 'Install Integration to get started', + }), + docsLink: policyTemplateNoDataConfig.docsLink, + actionHref, + actionTitle: policyTemplateNoDataConfig.actionTitle, + actionDescription: policyTemplateNoDataConfig.actionDescription, + testId: policyTemplateNoDataConfig.testId, + }; +}; + +const KIBANA_HEADERS_HEIGHT = 265; + +const IntegrationPostureDashboard = ({ + complianceData, + notInstalledConfig, + isIntegrationInstalled, +}: { + complianceData: ComplianceDashboardData | undefined; + notInstalledConfig: CspNoDataPageProps; + isIntegrationInstalled?: boolean; +}) => { + const noFindings = !complianceData || complianceData.stats.totalFindings === 0; + + // integration is not installed, and there are no findings for this integration + if (noFindings && !isIntegrationInstalled) { + return ; + } + + // integration is installed, but there are no findings for this integration + if (noFindings) { + return ( + // height is calculated for the screen height minus the kibana header, page title, and tabs +
+ } + title={ +

+ +

+ } + body={ +

+ +

+ } + /> +
+ ); + } + + // there are findings, displays dashboard even if integration is not installed + return ( + <> + + + + + + ); +}; export const ComplianceDashboard = () => { + const [selectedTab, setSelectedTab] = useState(CSPM_POLICY_TEMPLATE); const getSetupStatus = useCspSetupStatusApi(); const hasFindings = getSetupStatus.data?.status === 'indexed'; - const getDashboardData = useComplianceDashboardDataApi({ + const cspmIntegrationLink = useCspIntegrationLink(CSPM_POLICY_TEMPLATE); + const kspmIntegrationLink = useCspIntegrationLink(KSPM_POLICY_TEMPLATE); + + const getCspmDashboardData = useCspmStatsApi({ enabled: hasFindings, }); + const getKspmDashboardData = useKspmStatsApi({ + enabled: hasFindings, + }); + + useEffect(() => { + const selectInitialTab = () => { + const cspmTotalFindings = getCspmDashboardData.data?.stats.totalFindings; + const kspmTotalFindings = getKspmDashboardData.data?.stats.totalFindings; + const installedPolicyTemplates = getSetupStatus.data?.installedPolicyTemplates; + + let preferredDashboard = CSPM_POLICY_TEMPLATE; + + // cspm has findings + if (!!cspmTotalFindings) { + preferredDashboard = CSPM_POLICY_TEMPLATE; + } + // kspm has findings + else if (!!kspmTotalFindings) { + preferredDashboard = KSPM_POLICY_TEMPLATE; + } + // cspm is installed + else if (installedPolicyTemplates?.includes(CSPM_POLICY_TEMPLATE)) { + preferredDashboard = CSPM_POLICY_TEMPLATE; + } + // kspm is installed + else if (installedPolicyTemplates?.includes(KSPM_POLICY_TEMPLATE)) { + preferredDashboard = KSPM_POLICY_TEMPLATE; + } + + setSelectedTab(preferredDashboard); + }; + selectInitialTab(); + }, [ + getCspmDashboardData.data?.stats.totalFindings, + getKspmDashboardData.data?.stats.totalFindings, + getSetupStatus.data?.installedPolicyTemplates, + ]); + + const tabs = useMemo( + () => [ + { + label: i18n.translate('xpack.csp.dashboardTabs.cloudTab.tabTitle', { + defaultMessage: 'Cloud', + }), + isSelected: selectedTab === CSPM_POLICY_TEMPLATE, + onClick: () => setSelectedTab(CSPM_POLICY_TEMPLATE), + content: ( + +
+ +
+
+ ), + }, + { + label: i18n.translate('xpack.csp.dashboardTabs.kubernetesTab.tabTitle', { + defaultMessage: 'Kubernetes', + }), + isSelected: selectedTab === KSPM_POLICY_TEMPLATE, + onClick: () => setSelectedTab(KSPM_POLICY_TEMPLATE), + content: ( + +
+ +
+
+ ), + }, + ], + [ + cspmIntegrationLink, + getCspmDashboardData, + getKspmDashboardData, + getSetupStatus.data?.installedPolicyTemplates, + kspmIntegrationLink, + selectedTab, + ] + ); if (!hasFindings) return ; return ( - + { })} /> } + tabs={tabs.map(({ content, ...rest }) => rest)} />
- <> - - - - - + {tabs.find((t) => t.isSelected)?.content}
); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cloud_benchmarks_section.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cloud_benchmarks_section.tsx index 288a41d0a7a96..5ab9552d5d2da 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cloud_benchmarks_section.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/dashboard_sections/cloud_benchmarks_section.tsx @@ -49,7 +49,6 @@ export const CloudBenchmarksSection = ({ style={{ borderBottom: euiTheme.border.thick, borderBottomColor: euiTheme.colors.text, - marginBottom: euiTheme.size.m, paddingBottom: euiTheme.size.s, }} > @@ -91,51 +90,52 @@ export const CloudBenchmarksSection = ({
{complianceData.clusters.map((cluster) => ( - - - - - -
- - handleEvalCounterClick(cluster.meta.clusterId, evaluation) - } - /> -
-
- -
- - handleCellClick(cluster.meta.clusterId, resourceTypeName) - } - viewAllButtonTitle={i18n.translate( - 'xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle', - { defaultMessage: 'View all failed findings for this cluster' } - )} - onViewAllClick={() => handleViewAllClick(cluster.meta.clusterId)} - /> -
-
-
+ + + + + + +
+ + handleEvalCounterClick(cluster.meta.clusterId, evaluation) + } + /> +
+
+ +
+ + handleCellClick(cluster.meta.clusterId, resourceTypeName) + } + viewAllButtonTitle={i18n.translate( + 'xpack.csp.dashboard.risksTable.clusterCardViewAllButtonTitle', + { defaultMessage: 'View all failed findings for this cluster' } + )} + onViewAllClick={() => handleViewAllClick(cluster.meta.clusterId)} + /> +
+
+
+
))} ); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts index 50b4e7a2b03d3..264f848dd5886 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/test_subjects.ts @@ -7,6 +7,8 @@ export const MISSING_FINDINGS_NO_DATA_CONFIG = 'missing-findings-no-data-config'; export const DASHBOARD_CONTAINER = 'dashboard-container'; +export const KUBERNETES_DASHBOARD_CONTAINER = 'kubernetes-dashboard-container'; +export const CLOUD_DASHBOARD_CONTAINER = 'cloud-dashboard-container'; export const DASHBOARD_COUNTER_CARDS = { CLUSTERS_EVALUATED: 'dashboard-counter-card-clusters-evaluated', RESOURCES_EVALUATED: 'dashboard-counter-card-resources-evaluated', diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx index 70420f61d2176..02f2ec8ffee73 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings.test.tsx @@ -23,7 +23,7 @@ import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; import { createReactQueryResponse } from '../../test/fixtures/react_query'; import { useCISIntegrationPoliciesLink } from '../../common/navigation/use_navigate_to_cis_integration_policies'; -import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration'; +import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; import { NO_FINDINGS_STATUS_TEST_SUBJ } from '../../components/test_subjects'; import { render } from '@testing-library/react'; import { expectIdsInDoc } from '../../test/utils'; @@ -34,7 +34,8 @@ jest.mock('../../common/api/use_latest_findings_data_view'); jest.mock('../../common/api/use_setup_status_api'); jest.mock('../../common/hooks/use_subscription_status'); jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies'); -jest.mock('../../common/navigation/use_navigate_to_cis_integration'); +jest.mock('../../common/navigation/use_csp_integration_link'); + const chance = new Chance(); beforeEach(() => { @@ -74,7 +75,7 @@ describe('', () => { }) ); (useCISIntegrationPoliciesLink as jest.Mock).mockImplementation(() => chance.url()); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderFindingsPage(); @@ -96,7 +97,7 @@ describe('', () => { data: { status: 'indexing' }, }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderFindingsPage(); @@ -118,7 +119,7 @@ describe('', () => { data: { status: 'index-timeout' }, }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderFindingsPage(); @@ -140,7 +141,7 @@ describe('', () => { data: { status: 'unprivileged' }, }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); renderFindingsPage(); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx index 45e19b9fba5bd..762df0380a6d2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules.test.tsx @@ -19,14 +19,15 @@ import { createReactQueryResponse } from '../../test/fixtures/react_query'; import { coreMock } from '@kbn/core/public/mocks'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; -import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration'; +import { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; jest.mock('./use_csp_integration', () => ({ useCspIntegrationInfo: jest.fn(), })); jest.mock('../../common/api/use_setup_status_api'); jest.mock('../../common/hooks/use_subscription_status'); -jest.mock('../../common/navigation/use_navigate_to_cis_integration'); +jest.mock('../../common/navigation/use_csp_integration_link'); + const chance = new Chance(); const queryClient = new QueryClient({ @@ -78,7 +79,7 @@ describe('', () => { }) ); - (useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); }); it('calls API with URL params', async () => { diff --git a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts index 22097dbfc69fd..0e9cf02ad0663 100644 --- a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts @@ -9,7 +9,7 @@ import { SavedObjectsClientContract } from '@kbn/core/server'; import { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import { PackagePolicy } from '@kbn/fleet-plugin/common'; import { DataViewSavedObjectAttrs } from '@kbn/data-views-plugin/common'; -import { getCSPKuery } from '../../common/utils/helpers'; +import { CSP_FLEET_PACKAGE_KUERY } from '../../common/utils/helpers'; import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../../common/constants'; export const onPackagePolicyPostCreateCallback = async ( @@ -41,7 +41,7 @@ export const isCspPackagePolicyInstalled = async ( ): Promise => { try { const { total } = await packagePolicyClient.list(soClient, { - kuery: getCSPKuery, + kuery: CSP_FLEET_PACKAGE_KUERY, page: 1, }); diff --git a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts index 7d348f2d70250..ada8548240ae9 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/fleet_util.ts @@ -4,19 +4,22 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { uniq, map } from 'lodash'; +import { map, uniq } from 'lodash'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { - PackagePolicyClient, AgentPolicyServiceInterface, AgentService, + PackagePolicyClient, } from '@kbn/fleet-plugin/server'; import type { - GetAgentStatusResponse, - PackagePolicy, AgentPolicy, + GetAgentStatusResponse, ListResult, + PackagePolicy, } from '@kbn/fleet-plugin/common'; +import { PosturePolicyTemplate } from '../../common/types'; +import { SUPPORTED_POLICY_TEMPLATES } from '../../common/constants'; +import { CSP_FLEET_PACKAGE_KUERY } from '../../common/utils/helpers'; import { BENCHMARK_PACKAGE_POLICY_PREFIX, BenchmarksQueryParams, @@ -24,6 +27,9 @@ import { export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; +const isPolicyTemplate = (input: any): input is PosturePolicyTemplate => + SUPPORTED_POLICY_TEMPLATES.includes(input); + const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => { const integrationNameQuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`; const kquery = benchmarkFilter @@ -79,3 +85,28 @@ export const getCspPackagePolicies = ( sortOrder: queryParams.sort_order, }); }; + +export const getInstalledPolicyTemplates = async ( + packagePolicyClient: PackagePolicyClient, + soClient: SavedObjectsClientContract +) => { + try { + // getting all installed csp package policies + const queryResult = await packagePolicyClient.list(soClient, { + kuery: CSP_FLEET_PACKAGE_KUERY, + perPage: 1000, + }); + + // getting installed policy templates by findings enabled inputs + const enabledPolicyTemplates = queryResult.items + .map((policy) => { + return policy.inputs.find((input) => input.enabled)?.policy_template; + }) + .filter(isPolicyTemplate); + + // removing duplicates + return [...new Set(enabledPolicyTemplates)]; + } catch (e) { + return []; + } +}; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index 00c32f1b46f4a..eafacfac12f4e 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -69,7 +69,7 @@ const createBenchmarks = ( const agentPolicyStatus = { id: agentPolicy.id, name: agentPolicy.name, - agents: agentStatusByAgentPolicyId[agentPolicy.id].total, + agents: agentStatusByAgentPolicyId[agentPolicy.id]?.total, }; return { package_policy: cspPackage, 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 aa82b5844ce26..b59ad93d8f254 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 @@ -7,7 +7,8 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; -import type { ComplianceDashboardData } from '../../../common/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 { getGroupedFindingsEvaluation } from './get_grouped_findings_evaluation'; import { ClusterWithoutTrend, getClusters } from './get_clusters'; @@ -32,16 +33,23 @@ const getClustersTrends = (clustersWithoutTrends: ClusterWithoutTrend[], trends: const getSummaryTrend = (trends: Trends) => trends.map(({ timestamp, summary }) => ({ timestamp, ...summary })); +const queryParamsSchema = { + params: schema.object({ + // TODO: CIS AWS - replace with strict policy template values once available + policy_template: schema.string(), + }), +}; + export const defineGetComplianceDashboardRoute = (router: CspRouter): void => router.get( { path: STATS_ROUTE_PATH, - validate: false, + validate: queryParamsSchema, options: { tags: ['access:cloud-security-posture-read'], }, }, - async (context, _, response) => { + async (context, request, response) => { const cspContext = await context.csp; try { @@ -52,8 +60,13 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter): void => keep_alive: '30s', }); + const policyTemplate = request.params.policy_template as PosturePolicyTemplate; + const query: QueryDslQueryContainer = { - match_all: {}, + bool: { + // TODO: CIS AWS - replace filtered field to `policy_template` when available + filter: [{ term: { 'rule.benchmark.id': policyTemplate } }], + }, }; const [stats, groupedFindingsEvaluation, clustersWithoutTrends, trends] = await Promise.all( @@ -61,7 +74,7 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter): void => getStats(esClient, query, pitId), getGroupedFindingsEvaluation(esClient, query, pitId), getClusters(esClient, query, pitId), - getTrends(esClient), + getTrends(esClient, policyTemplate), ] ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts index 9c006a0ccbecc..aff4e39f9d49c 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.test.ts @@ -38,7 +38,7 @@ const oneIsZeroQueryResult: FindingsEvaluationsQueryResult = { const bothAreZeroQueryResult: FindingsEvaluationsQueryResult = { resources_evaluated: { - value: 30, + value: 0, }, failed_findings: { doc_count: 0, @@ -90,8 +90,13 @@ describe('getStatsFromFindingsEvaluationsAggs', () => { }); }); - it('should throw error if both evaluations are zero', async () => { - // const stats = getStatsFromFindingsEvaluationsAggs(bothAreZeroQueryResult); - expect(() => getStatsFromFindingsEvaluationsAggs(bothAreZeroQueryResult)).toThrow(); + it('should return zero on all stats if there are no failed or passed findings', async () => { + const stats = getStatsFromFindingsEvaluationsAggs(bothAreZeroQueryResult); + expect(stats).toEqual({ + totalFailed: 0, + totalPassed: 0, + totalFindings: 0, + postureScore: 0, + }); }); }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts index abb1ecd510ab6..d0fcd5b796774 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_stats.ts @@ -68,8 +68,7 @@ export const getStatsFromFindingsEvaluationsAggs = ( const failedFindings = findingsEvaluationsAggs.failed_findings.doc_count || 0; const passedFindings = findingsEvaluationsAggs.passed_findings.doc_count || 0; const totalFindings = failedFindings + passedFindings; - if (!totalFindings) throw new Error("couldn't calculate posture score"); - const postureScore = calculatePostureScore(passedFindings, failedFindings); + const postureScore = calculatePostureScore(passedFindings, failedFindings) || 0; return { totalFailed: failedFindings, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts index 270ce4f1ce177..a47b63e1fb921 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/get_trends.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient } from '@kbn/core/server'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS } from '../../../common/constants'; -import { Stats } from '../../../common/types'; +import type { PosturePolicyTemplate, Stats } from '../../../common/types'; import { calculatePostureScore } from './get_stats'; export interface ScoreTrendDoc { @@ -25,13 +25,20 @@ export interface ScoreTrendDoc { >; } -export const getTrendsQuery = () => ({ +export type Trends = Array<{ + timestamp: string; + summary: Stats; + clusters: Record; +}>; + +export const getTrendsQuery = (policyTemplate: PosturePolicyTemplate) => ({ index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, // large number that should be sufficient for 24 hours considering we write to the score index every 5 minutes size: 999, sort: '@timestamp:desc', query: { bool: { + filter: [{ term: { policy_template: policyTemplate } }], must: { range: { '@timestamp': { @@ -44,12 +51,6 @@ export const getTrendsQuery = () => ({ }, }); -export type Trends = Array<{ - timestamp: string; - summary: Stats; - clusters: Record; -}>; - export const getTrendsFromQueryResult = (scoreTrendDocs: ScoreTrendDoc[]): Trends => scoreTrendDocs.map((data) => ({ timestamp: data['@timestamp'], @@ -72,8 +73,11 @@ export const getTrendsFromQueryResult = (scoreTrendDocs: ScoreTrendDoc[]): Trend ), })); -export const getTrends = async (esClient: ElasticsearchClient): Promise => { - const trendsQueryResult = await esClient.search(getTrendsQuery()); +export const getTrends = async ( + esClient: ElasticsearchClient, + policyTemplate: PosturePolicyTemplate +): Promise => { + const trendsQueryResult = await esClient.search(getTrendsQuery(policyTemplate)); if (!trendsQueryResult.hits.hits) throw new Error('missing trend results from score index'); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts index 49dbf07883b23..33627389511a1 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/status/status.ts @@ -24,6 +24,7 @@ import { getAgentStatusesByAgentPolicies, getCspAgentPolicies, getCspPackagePolicies, + getInstalledPolicyTemplates, } from '../../lib/fleet_util'; import { checkIndexStatus } from '../../lib/check_index_status'; @@ -105,6 +106,7 @@ const getCspStatus = async ({ installation, latestCspPackage, installedPackagePolicies, + installedPolicyTemplates, ] = await Promise.all([ checkIndexStatus(esClient.asCurrentUser, LATEST_FINDINGS_INDEX_DEFAULT_NS, logger), checkIndexStatus(esClient.asCurrentUser, FINDINGS_INDEX_PATTERN, logger), @@ -114,6 +116,7 @@ const getCspStatus = async ({ getCspPackagePolicies(soClient, packagePolicyService, CLOUD_SECURITY_POSTURE_PACKAGE_NAME, { per_page: 10000, }), + getInstalledPolicyTemplates(packagePolicyService, soClient), ]); const healthyAgents = await getHealthyAgents( @@ -158,6 +161,7 @@ const getCspStatus = async ({ status, indicesDetails, latestPackageVersion: latestCspPackageVersion, + installedPolicyTemplates, healthyAgents, installedPackagePolicies: installedPackagePoliciesTotal, isPluginInitialized: isPluginInitialized(), @@ -168,6 +172,7 @@ const getCspStatus = async ({ indicesDetails, latestPackageVersion: latestCspPackageVersion, healthyAgents, + installedPolicyTemplates, installedPackagePolicies: installedPackagePoliciesTotal, installedPackageVersion: installation?.install_version, isPluginInitialized: isPluginInitialized(), 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 df1edb464c112..96a410d3cdaea 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,12 +14,7 @@ import { import { SearchRequest } from '@kbn/data-plugin/common'; import { ElasticsearchClient } from '@kbn/core/server'; import type { Logger } from '@kbn/core/server'; -import { - AggregatedFindingsByCluster, - ScoreBucket, - FindingsStatsTaskResult, - TaskHealthStatus, -} from './types'; +import { FindingsStatsTaskResult, TaskHealthStatus, ScoreByPolicyTemplateBucket } from './types'; import { BENCHMARK_SCORE_INDEX_DEFAULT_NS, LATEST_FINDINGS_INDEX_DEFAULT_NS, @@ -109,16 +104,81 @@ export function taskRunner(coreStartServices: CspServerPluginStartServices, logg }; } -const aggregateLatestFindings = async ( +const getScoreQuery = (): SearchRequest => ({ + index: LATEST_FINDINGS_INDEX_DEFAULT_NS, + size: 0, + query: { + match_all: {}, + }, + aggs: { + score_by_policy_template: { + terms: { + // TODO: CIS AWS - replace with policy_template when available + field: 'rule.benchmark.id', + }, + aggs: { + total_findings: { + value_count: { + field: 'result.evaluation', + }, + }, + passed_findings: { + filter: { + term: { + 'result.evaluation': 'passed', + }, + }, + }, + failed_findings: { + filter: { + term: { + 'result.evaluation': 'failed', + }, + }, + }, + score_by_cluster_id: { + terms: { + field: 'cluster_id', + }, + aggregations: { + total_findings: { + value_count: { + field: 'result.evaluation', + }, + }, + passed_findings: { + filter: { + term: { + 'result.evaluation': 'passed', + }, + }, + }, + failed_findings: { + filter: { + term: { + 'result.evaluation': 'failed', + }, + }, + }, + }, + }, + }, + }, + }, +}); + +export const aggregateLatestFindings = async ( esClient: ElasticsearchClient, stateRuns: number, logger: Logger ): Promise => { try { const startAggTime = performance.now(); - const evaluationsQueryResult = await esClient.search(getScoreQuery()); + const scoreIndexQueryResult = await esClient.search( + getScoreQuery() + ); - if (!evaluationsQueryResult.aggregations) { + if (!scoreIndexQueryResult.aggregations) { logger.warn(`No data found in latest findings index`); return 'warning'; } @@ -130,31 +190,45 @@ const aggregateLatestFindings = async ( ).toFixed(2)}ms]` ); - const clustersStats = Object.fromEntries( - evaluationsQueryResult.aggregations.score_by_cluster_id.buckets.map( - (clusterStats: AggregatedFindingsByCluster) => { + // getting score per policy template buckets + const scoresByPolicyTemplatesBuckets = + scoreIndexQueryResult.aggregations.score_by_policy_template.buckets; + + // iterating over the buckets and return promises which will index a modified document into the scores index + const docIndexingPromises = scoresByPolicyTemplatesBuckets.map((policyTemplateTrend) => { + // creating score per cluster id objects + const clustersStats = Object.fromEntries( + policyTemplateTrend.score_by_cluster_id.buckets.map((clusterStats) => { + const clusterId = clusterStats.key; + return [ - clusterStats.key, + clusterId, { total_findings: clusterStats.total_findings.value, passed_findings: clusterStats.passed_findings.doc_count, failed_findings: clusterStats.failed_findings.doc_count, }, ]; - } - ) - ); + }) + ); + + // each document contains the policy template and its scores + return esClient.index({ + index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, + document: { + policy_template: policyTemplateTrend.key, + passed_findings: policyTemplateTrend.passed_findings.doc_count, + failed_findings: policyTemplateTrend.failed_findings.doc_count, + total_findings: policyTemplateTrend.total_findings.value, + score_by_cluster_id: clustersStats, + }, + }); + }); const startIndexTime = performance.now(); - await esClient.index({ - index: BENCHMARK_SCORE_INDEX_DEFAULT_NS, - document: { - passed_findings: evaluationsQueryResult.aggregations.passed_findings.doc_count, - failed_findings: evaluationsQueryResult.aggregations.failed_findings.doc_count, - total_findings: evaluationsQueryResult.aggregations.total_findings.value, - score_by_cluster_id: clustersStats, - }, - }); + + // executing indexing commands + await Promise.all(docIndexingPromises); const totalIndexTime = Number(performance.now() - startIndexTime).toFixed(2); logger.debug( @@ -176,58 +250,3 @@ const aggregateLatestFindings = async ( return 'error'; } }; - -const getScoreQuery = (): SearchRequest => ({ - index: LATEST_FINDINGS_INDEX_DEFAULT_NS, - size: 0, - query: { - match_all: {}, - }, - aggs: { - total_findings: { - value_count: { - field: 'result.evaluation', - }, - }, - passed_findings: { - filter: { - term: { - 'result.evaluation': 'passed', - }, - }, - }, - failed_findings: { - filter: { - term: { - 'result.evaluation': 'failed', - }, - }, - }, - score_by_cluster_id: { - terms: { - field: 'cluster_id', - }, - aggregations: { - total_findings: { - value_count: { - field: 'result.evaluation', - }, - }, - passed_findings: { - filter: { - term: { - 'result.evaluation': 'passed', - }, - }, - }, - failed_findings: { - filter: { - term: { - 'result.evaluation': 'failed', - }, - }, - }, - }, - }, - }, -}); diff --git a/x-pack/plugins/cloud_security_posture/server/tasks/types.ts b/x-pack/plugins/cloud_security_posture/server/tasks/types.ts index 783d534b7d550..0e2ab6f655d4b 100644 --- a/x-pack/plugins/cloud_security_posture/server/tasks/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/tasks/types.ts @@ -5,18 +5,23 @@ * 2.0. */ -export interface AggregatedFindings { - passed_findings: { doc_count: number }; - failed_findings: { doc_count: number }; - total_findings: { value: number }; -} - -export interface AggregatedFindingsByCluster extends AggregatedFindings { - key: string; -} -export interface ScoreBucket extends AggregatedFindings { - score_by_cluster_id: { - buckets: AggregatedFindingsByCluster[]; +export interface ScoreByPolicyTemplateBucket { + score_by_policy_template: { + buckets: Array<{ + key: string; // policy template + doc_count: number; + passed_findings: { doc_count: number }; + failed_findings: { doc_count: number }; + total_findings: { value: number }; + score_by_cluster_id: { + buckets: Array<{ + key: string; // cluster id + passed_findings: { doc_count: number }; + failed_findings: { doc_count: number }; + total_findings: { value: number }; + }>; + }; + }>; }; } diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index e7379ba1e2a4b..b64a1192f3930 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -16,6 +16,7 @@ export const allowedExperimentalValues = Object.freeze({ packageVerification: true, showDevtoolsRequest: true, diagnosticFileUploadEnabled: false, + experimentalDataStreamSettings: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx index 4bfdff96a9d50..2191b151414ba 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/package_policy_input_stream.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { useRouteMatch } from 'react-router-dom'; -import { useGetDataStreams } from '../../../../../../../../hooks'; +import { useConfig, useGetDataStreams } from '../../../../../../../../hooks'; import { mapPackageReleaseToIntegrationCardRelease } from '../../../../../../../../services/package_prerelease'; import type { ExperimentalDataStreamFeature } from '../../../../../../../../../common/types/models/epm'; @@ -71,6 +71,10 @@ export const PackagePolicyInputStreamConfig = memo( inputStreamValidationResults, forceShowErrors, }) => { + const config = useConfig(); + const isExperimentalDataStreamSettingsEnabled = + config.enableExperimental?.includes('experimentalDataStreamSettings') ?? false; + const { params: { packagePolicyId }, } = useRouteMatch<{ packagePolicyId?: string }>(); @@ -305,13 +309,15 @@ export const PackagePolicyInputStreamConfig = memo( )} {/* Experimental index/datastream settings e.g. synthetic source */} - + {isExperimentalDataStreamSettingsEnabled && ( + + )} ) : null} diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 86104ab47c07a..63efd8494dbf8 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -8,6 +8,7 @@ export type { AsDuration, AsPercent, TimeUnitChar } from './utils/formatters'; export { formatDurationFromTimeUnitChar } from './utils/formatters'; +export { getInspectResponse } from './utils/get_inspect_response'; export { ProcessorEvent } from './processor_event'; diff --git a/x-pack/plugins/observability/public/hooks/use_es_search.ts b/x-pack/plugins/observability/public/hooks/use_es_search.ts index b25feef4d3f54..d99a7e80781c0 100644 --- a/x-pack/plugins/observability/public/hooks/use_es_search.ts +++ b/x-pack/plugins/observability/public/hooks/use_es_search.ts @@ -105,7 +105,10 @@ export const useEsSearch = , loading }; + return { + data: rawResponse as ESSearchResponse, + loading: Boolean(loading), + }; }; export function createEsParams(params: T): T { 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 6f08defd91ab8..bb341d7e54230 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 @@ -16,6 +16,7 @@ import { enrichSignalThreatMatches, groupAndMergeSignalMatches, getSignalMatchesFromThreatList, + 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'; @@ -802,7 +803,7 @@ describe('getSignalMatchesFromThreatList', () => { expect(signalMatches).toEqual([]); }); - it('return signal mathces from threat indicators', () => { + it('return signal matches from threat indicators', () => { const signalMatches = getSignalMatchesFromThreatList([ getThreatListItemMock({ _id: 'threatId', @@ -848,7 +849,7 @@ describe('getSignalMatchesFromThreatList', () => { ]); }); - it('merge signal mathces if different threat indicators matched the same signal', () => { + it('merge signal matches if different threat indicators matched the same signal', () => { const matchedQuery = [ encodeThreatMatchNamedQuery( getNamedQueryMock({ @@ -893,4 +894,26 @@ describe('getSignalMatchesFromThreatList', () => { }, ]); }); + + it('limits number of signal matches to MAX_NUMBER_OF_SIGNAL_MATCHES', () => { + const threatList = Array.from(Array(2000), (index) => + getThreatListItemMock({ + _id: `threatId-${index}`, + matched_queries: [ + encodeThreatMatchNamedQuery( + getNamedQueryMock({ + id: 'signalId1', + index: 'source_index', + value: 'threat.indicator.domain', + field: 'event.domain', + }) + ), + ], + }) + ); + + const signalMatches = getSignalMatchesFromThreatList(threatList); + + expect(signalMatches[0].queries).toHaveLength(MAX_NUMBER_OF_SIGNAL_MATCHES); + }); }); 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 1d8d68bd92d0d..e0b9d4fb6dee6 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 @@ -18,6 +18,8 @@ import type { } from './types'; import { extractNamedQueries } from './utils'; +export const MAX_NUMBER_OF_SIGNAL_MATCHES = 1000; + export const getSignalMatchesFromThreatList = ( threatList: ThreatListItem[] = [] ): SignalMatch[] => { @@ -34,6 +36,15 @@ export const getSignalMatchesFromThreatList = ( 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[signalId].length >= MAX_NUMBER_OF_SIGNAL_MATCHES) { + return; + } + signalMap[signalId].push({ id: threatHit._id, index: threatHit._index, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts index c075f95a9dc98..c2ee1fee3c75a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts @@ -20,6 +20,8 @@ import type { */ export const INDICATOR_PER_PAGE = 1000; +const MAX_NUMBER_OF_THREATS = 10 * 1000; + export const getThreatList = async ({ esClient, index, @@ -123,7 +125,9 @@ export const getAllThreatListHits = async ( allThreatListHits = allThreatListHits.concat(threatList.hits.hits); - while (threatList.hits.hits.length !== 0) { + // to prevent loading in memory large number of results, that could lead to out of memory Kibana crash, + // number of indicators is limited to MAX_NUMBER_OF_THREATS + while (threatList.hits.hits.length !== 0 && allThreatListHits.length < MAX_NUMBER_OF_THREATS) { threatList = await getThreatList({ ...params, searchAfter: threatList.hits.hits[threatList.hits.hits.length - 1].sort, diff --git a/x-pack/plugins/synthetics/common/constants/ui.ts b/x-pack/plugins/synthetics/common/constants/ui.ts index ad1c8b83736de..08752f08d231b 100644 --- a/x-pack/plugins/synthetics/common/constants/ui.ts +++ b/x-pack/plugins/synthetics/common/constants/ui.ts @@ -37,7 +37,7 @@ export const TEST_RUN_DETAILS_ROUTE = '/monitor/:monitorId/test-run/:checkGroupI export const MAPPING_ERROR_ROUTE = '/mapping-error'; -export const ERROR_DETAILS_ROUTE = '/error-details/:errorStateId'; +export const ERROR_DETAILS_ROUTE = '/monitor/:monitorId/errors/:errorStateId'; export enum STATUS { UP = 'up', diff --git a/x-pack/plugins/synthetics/kibana.json b/x-pack/plugins/synthetics/kibana.json index e3a0111fda77a..32bfa61698f98 100644 --- a/x-pack/plugins/synthetics/kibana.json +++ b/x-pack/plugins/synthetics/kibana.json @@ -28,7 +28,7 @@ "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": ["unifiedSearch", "fleet", "kibanaReact", "kibanaUtils", "ml", "observability", "indexLifecycleManagement"], + "requiredBundles": ["data","unifiedSearch", "fleet", "kibanaReact", "kibanaUtils", "ml", "observability", "indexLifecycleManagement"], "owner": { "name": "Uptime", "githubTeam": "uptime" diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/panel_with_title.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/panel_with_title.tsx new file mode 100644 index 0000000000000..9bc1cc480bc81 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/panel_with_title.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiPanel, EuiTitle, useEuiTheme, EuiPanelProps } from '@elastic/eui'; +import React from 'react'; + +export const PanelWithTitle: React.FC<{ title?: string } & EuiPanelProps> = ({ + title, + hasBorder = true, + hasShadow = false, + children, + ...props +}) => { + const { euiTheme } = useEuiTheme(); + + return ( + + {title && ( + +

{title}

+
+ )} + {children} +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_duration.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_duration.tsx new file mode 100644 index 0000000000000..9b65149be3ad0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_duration.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import moment from 'moment'; +import { useErrorFailedTests } from '../hooks/use_last_error_state'; + +export const ErrorDuration: React.FC = () => { + const { failedTests } = useErrorFailedTests(); + + const state = failedTests?.[0]?.state; + + const duration = state ? moment().diff(moment(state?.started_at), 'minutes') : 0; + + return ( + + ); +}; + +const ERROR_DURATION = i18n.translate('xpack.synthetics.errorDetails.errorDuration', { + defaultMessage: 'Error duration', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_started_at.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_started_at.tsx new file mode 100644 index 0000000000000..a6c3efb161711 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_started_at.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ReactElement } from 'react'; +import { EuiDescriptionList, EuiLoadingContent } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useErrorFailedTests } from '../hooks/use_last_error_state'; +import { useFormatTestRunAt } from '../../../utils/monitor_test_result/test_time_formats'; + +export const ErrorStartedAt: React.FC = () => { + const { failedTests } = useErrorFailedTests(); + + const state = failedTests?.[0]?.state; + + let startedAt: string | ReactElement = useFormatTestRunAt(state?.started_at); + + if (!startedAt) { + startedAt = ; + } + + return ; +}; + +const ERROR_DURATION = i18n.translate('xpack.synthetics.errorDetails.startedAt', { + defaultMessage: 'Started at', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_timeline.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_timeline.tsx new file mode 100644 index 0000000000000..30461842e963f --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/error_timeline.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { MonitorFailedTests } from '../../monitor_details/monitor_errors/failed_tests'; + +export const ErrorTimeline = () => { + return ; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/failed_tests_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/failed_tests_list.tsx new file mode 100644 index 0000000000000..9cf644153470a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/failed_tests_list.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { MouseEvent, useState } from 'react'; +import { EuiBasicTable, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { useHistory, useParams } from 'react-router-dom'; +import { useKibanaDateFormat } from '../../../../../hooks/use_kibana_date_format'; +import { Ping } from '../../../../../../common/runtime_types'; +import { + formatTestDuration, + formatTestRunAt, +} from '../../../utils/monitor_test_result/test_time_formats'; +import { useSyntheticsSettingsContext } from '../../../contexts'; + +export const FailedTestsList = ({ + failedTests, + loading, +}: { + failedTests: Ping[]; + loading?: boolean; +}) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(5); + const [sortField, setSortField] = useState('@timestamp'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + + const { monitorId } = useParams<{ monitorId: string }>(); + + const items = failedTests.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); + + const { basePath } = useSyntheticsSettingsContext(); + + const history = useHistory(); + + const format = useKibanaDateFormat(); + + const columns = [ + { + field: '@timestamp', + name: TIMESTAMP_LABEL, + sortable: true, + render: (value: string, item: Ping) => { + return ( + + {formatTestRunAt(value, format)} + + ); + }, + }, + { + field: 'monitor.duration.us', + name: MONITOR_DURATION_LABEL, + align: 'right' as const, + render: (value: number) => {formatTestDuration(value)}, + }, + ]; + + const pagination = { + pageIndex, + pageSize, + totalItemCount: failedTests.length, + pageSizeOptions: [3, 5, 8], + }; + + const getRowProps = (item: Ping) => { + const { state } = item; + if (state?.id) { + return { + 'data-test-subj': `row-${state.id}`, + onClick: (evt: MouseEvent) => { + history.push(`/monitor/${monitorId}/test-run/${item.monitor.check_group}`); + }, + }; + } + }; + + return ( +
+ + { + const { index: pIndex, size: pSize } = page; + + const { field: sField, direction: sDirection } = sort; + + setPageIndex(pIndex!); + setPageSize(pSize!); + setSortField(sField!); + setSortDirection(sDirection!); + }} + rowProps={getRowProps} + /> +
+ ); +}; + +const ERRORS_LIST_LABEL = i18n.translate('xpack.synthetics.errorsList.label', { + defaultMessage: 'Errors list', +}); + +const MONITOR_DURATION_LABEL = i18n.translate('xpack.synthetics.testDuration.label', { + defaultMessage: 'Test duration', +}); + +const TIMESTAMP_LABEL = i18n.translate('xpack.synthetics.timestamp.label', { + defaultMessage: '@timestamp', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/resolved_at.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/resolved_at.tsx new file mode 100644 index 0000000000000..75b1f9a31690b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/components/resolved_at.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { ReactElement } from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useErrorFailedTests } from '../hooks/use_last_error_state'; +import { useFormatTestRunAt } from '../../../utils/monitor_test_result/test_time_formats'; + +export const ResolvedAt: React.FC = () => { + const { failedTests } = useErrorFailedTests(); + + const state = failedTests?.[0]?.state; + + let endsAt: string | ReactElement = useFormatTestRunAt(state?.ends ?? ''); + + if (!endsAt) { + endsAt = 'N/A'; + } + + return ; +}; + +const ERROR_DURATION = i18n.translate('xpack.synthetics.errorDetails.resolvedAt', { + defaultMessage: 'Resolved at', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx index 01fbd377a04c9..c9d11d25defe2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/error_details_page.tsx @@ -5,13 +5,83 @@ * 2.0. */ -import { EuiLoadingLogo } from '@elastic/eui'; import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { StepDurationPanel } from '../monitor_details/monitor_summary/step_duration_panel'; +import { useFormatTestRunAt } from '../../utils/monitor_test_result/test_time_formats'; +import { LastTestRunComponent } from '../monitor_details/monitor_summary/last_test_run'; +import { MonitorDetailsPanel } from '../monitor_details/monitor_summary/monitor_details_panel'; +import { useStepDetails } from './hooks/use_step_details'; +import { StepDetails } from '../test_run_details/components/step_details'; +import { PanelWithTitle } from '../common/components/panel_with_title'; +import { useErrorFailedTests } from './hooks/use_error_failed_tests'; +import { useJourneySteps } from '../monitor_details/hooks/use_journey_steps'; +import { FailedTestsList } from './components/failed_tests_list'; +import { ErrorTimeline } from './components/error_timeline'; +import { useErrorDetailsBreadcrumbs } from './hooks/use_error_details_breadcrumbs'; +import { StepImage } from '../step_details_page/step_screenshot/step_image'; export function ErrorDetailsPage() { + const { failedTests, loading } = useErrorFailedTests(); + + const checkGroupId = failedTests?.[0]?.monitor.check_group ?? ''; + + const { + data, + isFailed, + failedStep, + stepLabels, + loading: stepsLoading, + } = useJourneySteps(checkGroupId); + + const lastTestRun = failedTests?.[0]; + + const startedAt = useFormatTestRunAt(lastTestRun?.state?.started_at); + + useErrorDetailsBreadcrumbs([{ text: startedAt }]); + + const stepDetails = useStepDetails({ checkGroup: lastTestRun?.monitor.check_group }); + return (
- TODO: + + + + + + + + + + + + + + + + + {data?.details?.journey && failedStep && ( + + )} + + + + + + + +
); } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_details_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_details_breadcrumbs.ts new file mode 100644 index 0000000000000..61cef2b818615 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_details_breadcrumbs.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useTestRunDetailsBreadcrumbs } from '../../test_run_details/hooks/use_test_run_details_breadcrumbs'; +import { useSelectedMonitor } from '../../monitor_details/hooks/use_selected_monitor'; +import { ConfigKey } from '../../../../../../common/runtime_types'; +import { PLUGIN } from '../../../../../../common/constants/plugin'; + +export const useErrorDetailsBreadcrumbs = ( + extraCrumbs?: Array<{ text: string; href?: string }> +) => { + const kibana = useKibana(); + const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; + + const { monitor } = useSelectedMonitor(); + + const errorsBreadcrumbs = [ + { + text: ERRORS_CRUMB, + href: `${appPath}/monitor/${monitor?.[ConfigKey.CONFIG_ID]}/errors`, + }, + ...(extraCrumbs ?? []), + ]; + + useTestRunDetailsBreadcrumbs(errorsBreadcrumbs); +}; + +const ERRORS_CRUMB = i18n.translate('xpack.synthetics.monitorsPage.errors', { + defaultMessage: 'Errors', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_failed_tests.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_failed_tests.tsx new file mode 100644 index 0000000000000..241c410038276 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_error_failed_tests.tsx @@ -0,0 +1,68 @@ +/* + * 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 { useEsSearch } from '@kbn/observability-plugin/public'; +import { useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { Ping } from '../../../../../../common/runtime_types'; +import { + EXCLUDE_RUN_ONCE_FILTER, + SUMMARY_FILTER, +} from '../../../../../../common/constants/client_defaults'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { useSyntheticsRefreshContext } from '../../../contexts'; +import { useGetUrlParams } from '../../../hooks'; + +export function useErrorFailedTests() { + const { lastRefresh } = useSyntheticsRefreshContext(); + + const { errorStateId, monitorId } = useParams<{ errorStateId: string; monitorId: string }>(); + + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); + + const { data, loading } = useEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 10000, + query: { + bool: { + filter: [ + SUMMARY_FILTER, + EXCLUDE_RUN_ONCE_FILTER, + { + term: { + 'state.id': errorStateId, + }, + }, + { + term: { + config_id: monitorId, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': 'desc' }], + }, + }, + [lastRefresh, monitorId, dateRangeStart, dateRangeEnd], + { name: 'getMonitorErrorFailedTests' } + ); + + return useMemo(() => { + const failedTests = + data?.hits.hits?.map((doc) => { + return doc._source as Ping; + }) ?? []; + + return { + failedTests, + loading, + }; + }, [data, loading]); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_last_error_state.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_last_error_state.tsx new file mode 100644 index 0000000000000..c0f100a3441ca --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_last_error_state.tsx @@ -0,0 +1,68 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { useReduxEsSearch } from '../../../hooks/use_redux_es_search'; +import { Ping } from '../../../../../../common/runtime_types'; +import { + EXCLUDE_RUN_ONCE_FILTER, + SUMMARY_FILTER, +} from '../../../../../../common/constants/client_defaults'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; +import { useSyntheticsRefreshContext } from '../../../contexts'; +import { useGetUrlParams } from '../../../hooks'; + +export function useErrorFailedTests() { + const { lastRefresh } = useSyntheticsRefreshContext(); + + const { errorStateId, monitorId } = useParams<{ errorStateId: string; monitorId: string }>(); + + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); + + const { data, loading } = useReduxEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 1000, + query: { + bool: { + filter: [ + SUMMARY_FILTER, + EXCLUDE_RUN_ONCE_FILTER, + { + term: { + 'state.id': errorStateId, + }, + }, + { + term: { + config_id: monitorId, + }, + }, + ], + }, + }, + sort: [{ '@timestamp': 'desc' }], + }, + }, + [lastRefresh, monitorId, dateRangeStart, dateRangeEnd], + { name: 'getMonitorErrorFailedTests' } + ); + + return useMemo(() => { + const failedTests = + data?.hits.hits?.map((doc) => { + return doc._source as Ping; + }) ?? []; + + return { + failedTests, + loading, + }; + }, [data, loading]); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_step_details.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_step_details.ts new file mode 100644 index 0000000000000..17111cabc0b78 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/hooks/use_step_details.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useJourneySteps } from '../../monitor_details/hooks/use_journey_steps'; + +export const useStepDetails = ({ checkGroup }: { checkGroup: string }) => { + const [stepIndex, setStepIndex] = React.useState(1); + + const { data: stepsData, loading: stepsLoading, stepEnds } = useJourneySteps(checkGroup); + + const step = stepEnds.find((stepN) => stepN.synthetics?.step?.index === stepIndex); + + const totalSteps = stepsLoading ? 1 : stepEnds.length; + + return { + step, + stepIndex, + setStepIndex, + totalSteps, + stepsData, + loading: stepsLoading, + }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/route_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/route_config.tsx new file mode 100644 index 0000000000000..fe2608fea7256 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/error_details/route_config.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useHistory } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { ResolvedAt } from './components/resolved_at'; +import { ErrorStartedAt } from './components/error_started_at'; +import { ErrorDetailsPage } from './error_details_page'; +import { ErrorDuration } from './components/error_duration'; +import { MonitorDetailsLocation } from '../monitor_details/monitor_details_location'; +import { ERROR_DETAILS_ROUTE } from '../../../../../common/constants'; +import { RouteProps } from '../../routes'; + +export const getErrorDetailsRouteConfig = ( + history: ReturnType, + syntheticsPath: string, + baseTitle: string +) => { + return { + title: i18n.translate('xpack.synthetics.errorDetailsRoute.title', { + defaultMessage: 'Error details | {baseTitle}', + values: { baseTitle }, + }), + path: ERROR_DETAILS_ROUTE, + component: ErrorDetailsPage, + dataTestSubj: 'syntheticsMonitorEditPage', + pageHeader: { + pageTitle: ( + + ), + rightSideItems: [ + , + , + , + , + ], + }, + } as RouteProps; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx index f8fc989c3cdc3..6fb0bf83801a3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_journey_steps.tsx @@ -31,18 +31,23 @@ export const useJourneySteps = (checkGroup?: string, lastRefresh?: number) => { step.synthetics?.step?.status === 'failed' || step.synthetics?.step?.status === 'skipped' ) ?? false; + const failedStep = data?.steps.find((step) => step.synthetics?.step?.status === 'failed'); + const stepEnds: JourneyStep[] = (data?.steps ?? []).filter(isStepEnd); const stepLabels = stepEnds.map((stepEnd) => stepEnd?.synthetics?.step?.name ?? ''); + const currentStep = stepIndex + ? data?.steps.find((step) => step.synthetics?.step?.index === Number(stepIndex)) + : undefined; + return { data: data as SyntheticsJourneyApiResponse, loading: loading ?? false, isFailed, stepEnds, stepLabels, - currentStep: stepIndex - ? data?.steps.find((step) => step.synthetics?.step?.index === Number(stepIndex)) - : undefined, + currentStep, + failedStep, }; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx index 2c989121d5ad9..04d825ce73720 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_monitor_latest_ping.tsx @@ -8,6 +8,7 @@ import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { ConfigKey } from '../../../../../../common/runtime_types'; +import { useSyntheticsRefreshContext } from '../../../contexts'; import { getMonitorLastRunAction, selectLastRunMetadata } from '../../../state'; import { useSelectedLocation } from './use_selected_location'; import { useSelectedMonitor } from './use_selected_monitor'; @@ -19,6 +20,7 @@ interface UseMonitorLatestPingParams { export const useMonitorLatestPing = (params?: UseMonitorLatestPingParams) => { const dispatch = useDispatch(); + const { lastRefresh } = useSyntheticsRefreshContext(); const { monitor } = useSelectedMonitor(); const location = useSelectedLocation(); @@ -41,7 +43,7 @@ export const useMonitorLatestPing = (params?: UseMonitorLatestPingParams) => { if (monitorId && locationLabel && !isUpToDate) { dispatch(getMonitorLastRunAction.get({ monitorId, locationId: locationLabel })); } - }, [dispatch, monitorId, locationLabel, isUpToDate]); + }, [dispatch, monitorId, locationLabel, isUpToDate, lastRefresh]); if (!monitorId || !locationLabel) { return { loading, latestPing: undefined }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_location.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_location.tsx index eb93b713b9cf7..d41b086010482 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_location.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_details_location.tsx @@ -11,6 +11,7 @@ import { EuiHealth, EuiIcon, EuiLink, + EuiLoadingContent, EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -120,7 +121,11 @@ export const MonitorDetailsLocation: React.FC = () => { ]); if (!selectedLocation || !monitor) { - return null; + return ( + }]} + /> + ); } return ; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx index 12443d7896c04..48f6ea76b12ed 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/errors_list.tsx @@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n'; import React, { MouseEvent, useMemo, useState } from 'react'; import { EuiBasicTable, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useParams } from 'react-router-dom'; +import { useSelectedLocation } from '../hooks/use_selected_location'; import { useKibanaDateFormat } from '../../../../../hooks/use_kibana_date_format'; import { Ping } from '../../../../../../common/runtime_types'; import { useErrorFailedStep } from '../hooks/use_error_failed_step'; @@ -27,6 +28,8 @@ export const ErrorsList = () => { const { errorStates, loading } = useMonitorErrors(); + const { monitorId } = useParams<{ monitorId: string }>(); + const items = errorStates.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); const checkGroups = useMemo(() => { @@ -45,6 +48,8 @@ export const ErrorsList = () => { const format = useKibanaDateFormat(); + const selectedLocation = useSelectedLocation(); + const columns = [ { field: '@timestamp', @@ -52,7 +57,14 @@ export const ErrorsList = () => { sortable: true, render: (value: string, item: Ping) => { return ( - + {formatTestRunAt(item.state!.started_at, format)} ); @@ -99,7 +111,9 @@ export const ErrorsList = () => { height: '85px', 'data-test-subj': `row-${state.id}`, onClick: (evt: MouseEvent) => { - history.push(`/error-details/${state.id}`); + history.push( + `/monitor/${monitorId}/errors/${state.id}?locationId=${selectedLocation?.id}` + ); }, }; } @@ -136,6 +150,20 @@ export const ErrorsList = () => { ); }; +export const getErrorDetailsUrl = ({ + basePath, + monitorId, + stateId, + locationId, +}: { + stateId: string; + basePath: string; + monitorId: string; + locationId: string; +}) => { + return `${basePath}/app/synthetics/monitor/${monitorId}/errors/${stateId}?locationId=${locationId}`; +}; + const ERRORS_LIST_LABEL = i18n.translate('xpack.synthetics.errorsList.label', { defaultMessage: 'Errors list', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx index c801036e78fd5..b45428f292c24 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useParams } from 'react-router-dom'; import { useMonitorQueryId } from '../hooks/use_monitor_query_id'; import { ClientPluginsStart } from '../../../../../plugin'; @@ -17,7 +18,9 @@ export const MonitorFailedTests = ({ time }: { time: { to: string; from: string const monitorId = useMonitorQueryId(); - if (!monitorId) { + const { errorStateId } = useParams<{ errorStateId: string }>(); + + if (!monitorId && !errorStateId) { return null; } @@ -31,7 +34,7 @@ export const MonitorFailedTests = ({ time }: { time: { to: string; from: string { time, reportDefinitions: { - 'monitor.id': [monitorId], + ...(monitorId ? { 'monitor.id': [monitorId] } : { 'state.id': [errorStateId] }), }, dataType: 'synthetics', selectedMetricField: 'failed_tests', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx index ef5d3bd535a54..b3aa2e129dc81 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/last_test_run.tsx @@ -22,11 +22,14 @@ import { import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; +import { useSelectedLocation } from '../hooks/use_selected_location'; +import { getErrorDetailsUrl } from '../monitor_errors/errors_list'; import { ConfigKey, DataStream, EncryptedSyntheticsSavedMonitor, Ping, + SyntheticsJourneyApiResponse, } from '../../../../../../common/runtime_types'; import { formatTestRunAt } from '../../../utils/monitor_test_result/test_time_formats'; @@ -41,10 +44,8 @@ import { useSelectedMonitor } from '../hooks/use_selected_monitor'; import { useMonitorLatestPing } from '../hooks/use_monitor_latest_ping'; export const LastTestRun = () => { - const { euiTheme } = useEuiTheme(); const { latestPing, loading: pingsLoading } = useMonitorLatestPing(); const { lastRefresh } = useSyntheticsRefreshContext(); - const { monitor } = useSelectedMonitor(); const { data: stepsData, loading: stepsLoading } = useJourneySteps( latestPing?.monitor?.check_group, @@ -53,6 +54,35 @@ export const LastTestRun = () => { const loading = stepsLoading || pingsLoading; + return ( + + ); +}; + +export const LastTestRunComponent = ({ + latestPing, + loading, + stepsData, + stepsLoading, + isErrorDetails = false, +}: { + stepsLoading: boolean; + latestPing?: Ping; + loading: boolean; + stepsData: SyntheticsJourneyApiResponse; + isErrorDetails?: boolean; +}) => { + const { monitor } = useSelectedMonitor(); + const { euiTheme } = useEuiTheme(); + + const selectedLocation = useSelectedLocation(); + const { basePath } = useSyntheticsSettingsContext(); + return ( @@ -69,11 +99,24 @@ export const LastTestRun = () => { color="danger" iconType="alert" > - - {i18n.translate('xpack.synthetics.monitorDetails.summary.viewErrorDetails', { - defaultMessage: 'View error details', - })} - + {isErrorDetails ? ( + <> + ) : ( + + {i18n.translate('xpack.synthetics.monitorDetails.summary.viewErrorDetails', { + defaultMessage: 'View error details', + })} + + )} ) : null} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx index b60be67942e4a..c84bfb4e0978b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx @@ -18,7 +18,13 @@ import { ClientPluginsStart } from '../../../../../plugin'; import { useSelectedLocation } from '../hooks/use_selected_location'; import { useAbsoluteDate } from '../../../hooks'; -export const StepDurationPanel = ({ legendPosition }: { legendPosition?: Position }) => { +export const StepDurationPanel = ({ + legendPosition, + doBreakdown = true, +}: { + legendPosition?: Position; + doBreakdown?: boolean; +}) => { const { observability } = useKibana().services; const time = useAbsoluteDate({ from: 'now-24h/h', to: 'now' }); @@ -40,12 +46,18 @@ export const StepDurationPanel = ({ legendPosition }: { legendPosition?: Positio return null; } + const label = !doBreakdown + ? MONITOR_DURATION + : isBrowser + ? DURATION_BY_STEP_LABEL + : DURATION_BY_LOCATION; + return ( -

{isBrowser ? DURATION_BY_STEP_LABEL : DURATION_BY_LOCATION}

+

{label}

@@ -60,19 +72,23 @@ export const StepDurationPanel = ({ legendPosition }: { legendPosition?: Positio customHeight={'300px'} reportType={ReportTypes.KPI} legendPosition={legendPosition} + legendIsVisible={doBreakdown} attributes={[ { time, - name: DURATION_BY_STEP_LABEL, + name: label, reportDefinitions: { 'monitor.id': [monitorId], 'observer.geo.name': [selectedLocation?.label], }, - selectedMetricField: isBrowser ? 'synthetics.step.duration.us' : 'monitor.duration.us', + selectedMetricField: + isBrowser && doBreakdown ? 'synthetics.step.duration.us' : 'monitor.duration.us', dataType: 'synthetics', - breakdown: isBrowser ? 'synthetics.step.name.keyword' : 'observer.geo.name', - operationType: 'last_value', + operationType: doBreakdown ? 'last_value' : 'average', seriesType: 'area_stacked', + ...(doBreakdown + ? { breakdown: isBrowser ? 'synthetics.step.name.keyword' : 'observer.geo.name' } + : {}), }, ]} /> @@ -88,6 +104,10 @@ const DURATION_BY_LOCATION = i18n.translate('xpack.synthetics.detailsPanel.durat defaultMessage: 'Duration by location', }); +const MONITOR_DURATION = i18n.translate('xpack.synthetics.detailsPanel.monitorDuration', { + defaultMessage: 'Monitor duration', +}); + const LAST_24H_LABEL = i18n.translate('xpack.synthetics.detailsPanel.last24Hours', { defaultMessage: 'Last 24 hours', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/last_successful_screenshot.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/last_successful_screenshot.tsx index 0541e0a091e71..a3abaa6b8b8de 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/last_successful_screenshot.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/last_successful_screenshot.tsx @@ -14,14 +14,20 @@ import { JourneyStep } from '../../../../../../common/runtime_types'; import { EmptyImage } from '../../common/screenshot/empty_image'; import { JourneyStepScreenshotContainer } from '../../common/screenshot/journey_step_screenshot_container'; -export const LastSuccessfulScreenshot = ({ step }: { step: JourneyStep }) => { - const { stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>(); +export const LastSuccessfulScreenshot = ({ + step, + stepIndex: stepInd, +}: { + step: JourneyStep; + stepIndex?: number; +}) => { + const { stepIndex } = useParams<{ checkGroupId: string; stepIndex?: string }>(); const { data, loading } = useFetcher(() => { return fetchLastSuccessfulCheck({ timestamp: step['@timestamp'], monitorId: step.monitor.id, - stepIndex: Number(stepIndex), + stepIndex: Number(stepIndex ?? stepInd), location: step.observer?.geo?.name, }); }, [step._id, step['@timestamp']]); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/step_image.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/step_image.tsx index 8bc05a25dec45..161b41f5bac18 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/step_image.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_screenshot/step_image.tsx @@ -58,7 +58,7 @@ export const StepImage = ({ asThumbnail={false} /> ) : ( - + )} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/components/step_details.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/components/step_details.tsx new file mode 100644 index 0000000000000..79a6c3d9081bc --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/components/step_details.tsx @@ -0,0 +1,67 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { JourneyStep, SyntheticsJourneyApiResponse } from '../../../../../../common/runtime_types'; +import { StepNumberNav } from './step_number_nav'; +import { StepScreenshotDetails } from '../step_screenshot_details'; +import { StepTabs } from '../step_tabs'; + +export const StepDetails = ({ + step, + loading, + stepIndex, + stepsData, + totalSteps, + setStepIndex, +}: { + loading: boolean; + step?: JourneyStep; + stepsData: SyntheticsJourneyApiResponse; + stepIndex: number; + totalSteps: number; + setStepIndex: (stepIndex: number) => void; +}) => { + return ( + + + + +

+ +

+
+
+ + { + setStepIndex(stepIndex + 1); + }} + handlePreviousStep={() => { + setStepIndex(stepIndex - 1); + }} + /> + +
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/hooks/use_test_run_details_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/hooks/use_test_run_details_breadcrumbs.ts index f3b80987b2227..ea2d3475a4781 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/hooks/use_test_run_details_breadcrumbs.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/hooks/use_test_run_details_breadcrumbs.ts @@ -20,7 +20,6 @@ export const useTestRunDetailsBreadcrumbs = ( const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; const { monitor } = useSelectedMonitor(); - const selectedLocation = useSelectedLocation(); useBreadcrumbs([ diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx index 5219fef84d2a9..c416d71168af1 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/test_run_details/step_screenshot_details.tsx @@ -32,7 +32,7 @@ export const StepScreenshotDetails = ({ { // Step index from starts at 1 in synthetics @@ -35,40 +32,14 @@ export const TestRunDetails = () => { return ( - - - - -

- -

-
-
- - { - setStepIndex(stepIndex + 1); - }} - handlePreviousStep={() => { - setStepIndex(stepIndex - 1); - }} - /> - -
- - - - -
+
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts new file mode 100644 index 0000000000000..72715c596e82a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_redux_es_search.ts @@ -0,0 +1,54 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { ESSearchResponse } from '@kbn/es-types'; +import { IInspectorInfo } from '@kbn/data-plugin/common'; +import { useInspectorContext } from '@kbn/observability-plugin/public'; +import { useDispatch, useSelector } from 'react-redux'; +import { useEffect, useMemo } from 'react'; +import { + executeEsQueryAction, + selectEsQueryLoading, + selectEsQueryResult, +} from '../state/elasticsearch'; + +export const useReduxEsSearch = < + DocumentSource extends unknown, + TParams extends estypes.SearchRequest +>( + params: TParams, + fnDeps: any[], + options: { inspector?: IInspectorInfo; name: string } +) => { + const { name } = options ?? {}; + + const { addInspectorRequest } = useInspectorContext(); + + const dispatch = useDispatch(); + + const loadings = useSelector(selectEsQueryLoading); + const results = useSelector(selectEsQueryResult); + + useEffect(() => { + if (params.index) { + dispatch(executeEsQueryAction.get({ params, name, addInspectorRequest })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [addInspectorRequest, dispatch, name, JSON.stringify(params)]); + + return useMemo(() => { + return { + data: results[name] as ESSearchResponse, + loading: loadings[name], + }; + }, [loadings, name, results]); +}; + +export function createEsParams(params: T): T { + return params; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 7f10a3598dbf2..9d938e4ce6b37 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -18,7 +18,6 @@ import { useInspectorContext } from '@kbn/observability-plugin/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public'; import { getSettingsRouteConfig } from './components/settings/route_config'; import { TestRunDetails } from './components/test_run_details/test_run_details'; -import { ErrorDetailsPage } from './components/error_details/error_details_page'; import { StepTitle } from './components/step_details_page/step_title'; import { MonitorAddPageWithServiceAllowed } from './components/monitor_add_edit/monitor_add_page'; import { MonitorEditPageWithServiceAllowed } from './components/monitor_add_edit/monitor_edit_page'; @@ -42,7 +41,6 @@ import { MONITOR_ERRORS_ROUTE, MONITOR_HISTORY_ROUTE, MONITOR_ROUTE, - ERROR_DETAILS_ROUTE, STEP_DETAIL_ROUTE, OVERVIEW_ROUTE, TEST_RUN_DETAILS_ROUTE, @@ -58,6 +56,7 @@ import { MonitorSummary } from './components/monitor_details/monitor_summary/mon import { MonitorHistory } from './components/monitor_details/monitor_history/monitor_history'; import { MonitorErrors } from './components/monitor_details/monitor_errors/monitor_errors'; import { StepDetailPage } from './components/step_details_page/step_detail_page'; +import { getErrorDetailsRouteConfig } from './components/error_details/route_config'; export type RouteProps = LazyObservabilityPageTemplateProps & { path: string; @@ -84,6 +83,7 @@ const getRoutes = ( ): RouteProps[] => { return [ ...getSettingsRouteConfig(history, syntheticsPath, baseTitle), + getErrorDetailsRouteConfig(history, syntheticsPath, baseTitle), { title: i18n.translate('xpack.synthetics.gettingStartedRoute.title', { defaultMessage: 'Synthetics Getting Started | {baseTitle}', @@ -285,23 +285,6 @@ const getRoutes = ( ], }, }, - { - title: i18n.translate('xpack.synthetics.errorDetailsRoute.title', { - defaultMessage: 'Error details | {baseTitle}', - values: { baseTitle }, - }), - path: ERROR_DETAILS_ROUTE, - component: ErrorDetailsPage, - dataTestSubj: 'syntheticsMonitorEditPage', - pageHeader: { - pageTitle: ( - - ), - }, - }, { title: i18n.translate('xpack.synthetics.testRunDetailsRoute.title', { defaultMessage: 'Test run details | {baseTitle}', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/actions.ts new file mode 100644 index 0000000000000..fbf505ecc950d --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/actions.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as esTypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ESSearchResponse } from '@kbn/es-types'; +import { FetcherResult } from '@kbn/observability-plugin/public/hooks/use_fetcher'; +import { createAsyncAction } from '../utils/actions'; + +export const executeEsQueryAction = createAsyncAction< + { + params: esTypes.SearchRequest; + name: string; + addInspectorRequest: (result: FetcherResult) => void; + }, + { name: string; result: ESSearchResponse } +>('executeEsQueryAction'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/api.ts new file mode 100644 index 0000000000000..6f233b89423b2 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/api.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '@kbn/data-plugin/common'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ESSearchResponse } from '@kbn/es-types'; +import { FETCH_STATUS } from '@kbn/observability-plugin/public'; +import { getInspectResponse } from '@kbn/observability-plugin/common'; +import type { FetcherResult } from '@kbn/observability-plugin/public/hooks/use_fetcher'; +import { kibanaService } from '../../../../utils/kibana_service'; + +export const executeEsQueryAPI = async ({ + params, + name, + addInspectorRequest, +}: { + params: estypes.SearchRequest; + name: string; + addInspectorRequest: (result: FetcherResult) => void; +}) => { + const data = kibanaService.startPlugins.data; + + const response = new Promise((resolve, reject) => { + const startTime = Date.now(); + + const search$ = data.search + .search( + { + params, + }, + {} + ) + .subscribe({ + next: (result) => { + if (isCompleteResponse(result)) { + if (addInspectorRequest) { + addInspectorRequest({ + data: { + _inspect: [ + getInspectResponse({ + startTime, + esRequestParams: params, + esResponse: result.rawResponse, + esError: null, + esRequestStatus: 1, + operationName: name, + kibanaRequest: { + route: { + path: '/internal/bsearch', + method: 'POST', + }, + } as any, + }), + ], + }, + status: FETCH_STATUS.SUCCESS, + }); + } + // Final result + resolve(result); + search$.unsubscribe(); + } + }, + error: (err) => { + if (isErrorResponse(err)) { + // eslint-disable-next-line no-console + console.error(err); + reject(err); + if (addInspectorRequest) { + addInspectorRequest({ + data: { + _inspect: [ + getInspectResponse({ + startTime, + esRequestParams: params, + esResponse: null, + esError: { originalError: err, name: err.name, message: err.message }, + esRequestStatus: 2, + operationName: name, + kibanaRequest: { + route: { + path: '/internal/bsearch', + method: 'POST', + }, + } as any, + }), + ], + }, + status: FETCH_STATUS.SUCCESS, + }); + } + } + }, + }); + }); + + const { rawResponse } = await response; + return { result: rawResponse as ESSearchResponse, name }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/effects.ts new file mode 100644 index 0000000000000..d92285e537dc6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/effects.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 { takeLeading } from 'redux-saga/effects'; + +import { fetchEffectFactory } from '../utils/fetch_effect'; +import { executeEsQueryAction } from './actions'; +import { executeEsQueryAPI } from './api'; + +export function* executeEsQueryEffect() { + yield takeLeading( + executeEsQueryAction.get, + fetchEffectFactory(executeEsQueryAPI, executeEsQueryAction.success, executeEsQueryAction.fail) + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/index.ts new file mode 100644 index 0000000000000..1fbb4e352715b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/index.ts @@ -0,0 +1,46 @@ +/* + * 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 { createReducer } from '@reduxjs/toolkit'; +import { ESSearchResponse } from '@kbn/es-types'; + +import { IHttpSerializedFetchError } from '..'; +import { executeEsQueryAction } from './actions'; + +export interface QueriesState { + results: Record; + loading: Record; + error: Record; +} + +const initialState: QueriesState = { + results: {}, + loading: {}, + error: {}, +}; + +export const elasticsearchReducer = createReducer(initialState, (builder) => { + builder + .addCase(executeEsQueryAction.get, (state, action) => { + const name = action.payload.name; + state.loading = { ...state.loading, [name]: true }; + }) + .addCase(executeEsQueryAction.success, (state, action) => { + const name = action.payload.name; + state.loading = { ...state.loading, [name]: false }; + state.results = { ...state.results, [name]: action.payload.result }; + }) + .addCase(executeEsQueryAction.fail, (state, action) => { + const name = action.payload.name; + state.loading = { ...state.loading, [name]: false }; + state.error = { ...state.error, [name]: action.payload }; + }); +}); + +export * from './actions'; +export * from './effects'; +export * from './selectors'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/selectors.ts new file mode 100644 index 0000000000000..b978908edc305 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/elasticsearch/selectors.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SyntheticsAppState } from '../root_reducer'; + +export const selectEsQueryLoading = (queryState: SyntheticsAppState) => + queryState.elasticsearch.loading; + +export const selectEsQueryResult = (queryState: SyntheticsAppState) => + queryState.elasticsearch.results; + +export const selectEsQueryError = (queryState: SyntheticsAppState) => + queryState.elasticsearch.error; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts index 943fa677367cd..f447f56c4aa63 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts @@ -7,6 +7,7 @@ import { all, fork } from 'redux-saga/effects'; import { enableDefaultAlertingEffect, updateDefaultAlertingEffect } from './alert_rules/effects'; +import { executeEsQueryEffect } from './elasticsearch'; import { fetchAlertConnectorsEffect, fetchDynamicSettingsEffect, @@ -50,5 +51,6 @@ export const rootEffect = function* root(): Generator { fork(enableDefaultAlertingEffect), fork(enableMonitorAlertEffect), fork(updateDefaultAlertingEffect), + fork(executeEsQueryEffect), ]); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts index 72c6d693149dc..cc32d2d709dc6 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts @@ -9,8 +9,13 @@ import { combineReducers } from '@reduxjs/toolkit'; import { browserJourneyReducer } from './browser_journey'; import { defaultAlertingReducer, DefaultAlertingState } from './alert_rules'; -import { dynamicSettingsReducer, DynamicSettingsState, settingsReducer } from './settings'; -import { SettingsState } from './settings'; +import { + dynamicSettingsReducer, + DynamicSettingsState, + settingsReducer, + SettingsState, +} from './settings'; +import { elasticsearchReducer, QueriesState } from './elasticsearch'; import { agentPoliciesReducer, AgentPoliciesState } from './private_locations'; import { networkEventsReducer, NetworkEventsState } from './network_events'; import { monitorDetailsReducer, MonitorDetailsState } from './monitor_details'; @@ -27,6 +32,7 @@ export interface SyntheticsAppState { ui: UiState; settings: SettingsState; pingStatus: PingStatusState; + elasticsearch: QueriesState; monitorList: MonitorListState; indexStatus: IndexStatusState; overview: MonitorOverviewState; @@ -34,9 +40,9 @@ export interface SyntheticsAppState { agentPolicies: AgentPoliciesState; monitorDetails: MonitorDetailsState; browserJourney: BrowserJourneyState; - serviceLocations: ServiceLocationsState; dynamicSettings: DynamicSettingsState; defaultAlerting: DefaultAlertingState; + serviceLocations: ServiceLocationsState; syntheticsEnablement: SyntheticsEnablementState; } @@ -48,6 +54,7 @@ export const rootReducer = combineReducers({ indexStatus: indexStatusReducer, overview: monitorOverviewReducer, networkEvents: networkEventsReducer, + elasticsearch: elasticsearchReducer, agentPolicies: agentPoliciesReducer, monitorDetails: monitorDetailsReducer, browserJourney: browserJourneyReducer, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx index b70342e03dee4..a5846f7a4eda8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -63,6 +63,7 @@ const Application = (props: SyntheticsAppProps) => { }, [canSave, renderGlobalHelpControls, setBadge]); kibanaService.core = core; + kibanaService.startPlugins = startPlugins; kibanaService.theme = props.appMountParameters.theme$; store.dispatch(setBasePath(basePath)); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts index ce9be57fe313d..511dc0b0c9b99 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/monitor_test_result/test_time_formats.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; +import { useKibanaDateFormat } from '../../../../hooks/use_kibana_date_format'; /** * Formats the microseconds (µ) into either milliseconds (ms) or seconds (s) based on the duration value @@ -34,3 +35,11 @@ export function formatTestRunAt(timestamp: string, format: string) { const stampedMoment = moment(timestamp); return stampedMoment.format(format); } + +export function useFormatTestRunAt(timestamp?: string) { + const format = useKibanaDateFormat(); + if (!timestamp) { + return ''; + } + return formatTestRunAt(timestamp, format); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index ca65c80a97cb3..8e0aa77e55f00 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -129,6 +129,11 @@ export const mockState: SyntheticsAppState = { error: null, success: null, }, + elasticsearch: { + results: {}, + loading: {}, + error: {}, + }, }; function getBrowserJourneyMockSlice() { diff --git a/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts b/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts index 06fa2b892b4a0..021d8c7ec3d7d 100644 --- a/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts +++ b/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts @@ -7,11 +7,13 @@ import type { Observable } from 'rxjs'; import type { CoreStart, CoreTheme } from '@kbn/core/public'; +import { ClientPluginsStart } from '../../plugin'; import { apiService } from '../api_service/api_service'; class KibanaService { private static instance: KibanaService; private _core!: CoreStart; + private _startPlugins!: ClientPluginsStart; private _theme!: Observable; public get core() { @@ -23,6 +25,14 @@ class KibanaService { apiService.http = this._core.http; } + public get startPlugins() { + return this._startPlugins; + } + + public set startPlugins(startPlugins: ClientPluginsStart) { + this._startPlugins = startPlugins; + } + public get theme() { return this._theme; } diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_details.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_details.ts index 8eeb744eb3a0d..be69da15f389e 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_details.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_details.ts @@ -40,7 +40,10 @@ export const getJourneyDetails: UMElasticsearchQueryFn< size: 1, }; - const { body: thisJourney } = await uptimeEsClient.search({ body: baseParams }); + const { body: thisJourney } = await uptimeEsClient.search( + { body: baseParams }, + 'getJourneyDetails' + ); if (thisJourney.hits.hits.length > 0) { const { _id, _source } = thisJourney.hits.hits[0]; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_steps.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_steps.ts index e2aea3ede8747..c200a4c22a671 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_steps.ts @@ -63,7 +63,7 @@ export const getJourneySteps: UMElasticsearchQueryFn< }, size: 500, }; - const { body: result } = await uptimeEsClient.search({ body: params }); + const { body: result } = await uptimeEsClient.search({ body: params }, 'getJourneySteps'); const steps = result.hits.hits.map( ({ _id, _source }) => Object.assign({ _id }, _source) as ResultType diff --git a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.ts b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.ts index 23088f7f8c0f1..379618b998f92 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_indicator_by_id.ts @@ -34,10 +34,8 @@ export const useIndicatorById = (indicatorId: string) => { bool: { must: [ { - term: { - _id: { - value: indicatorId, - }, + ids: { + values: [indicatorId], }, }, ], @@ -51,7 +49,6 @@ export const useIndicatorById = (indicatorId: string) => { ]; const req = { params: { - index: ['filebeat-*'], body: { query, fields, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 1303231a61719..ee24d3f439f65 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -10148,7 +10148,6 @@ "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " pour \"{name}\"", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "Affichage de {pageCount} sur {totalCount, plural, one {# intégration} other {# intégrations}}", "xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode} : {body}", - "xpack.csp.cloudPosturePage.packageNotInstalled.description": "Utilisez notre intégration {integrationFullName} (KSPM) pour mesurer votre configuration de cluster Kubernetes par rapport aux recommandations du CIS.", "xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}", "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}", @@ -10179,8 +10178,6 @@ "xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel": "Niveau de sécurité du cloud", "xpack.csp.cloudPosturePage.errorRenderer.errorTitle": "Nous n'avons pas pu récupérer vos données sur le niveau de sécurité du cloud.", "xpack.csp.cloudPosturePage.loadingDescription": "Chargement...", - "xpack.csp.cloudPosturePage.packageNotInstalled.buttonLabel": "Ajouter une intégration KSPM", - "xpack.csp.cloudPosturePage.packageNotInstalled.integrationNameLabel": "Gestion du niveau de sécurité Kubernetes", "xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle": "Installer l'intégration pour commencer", "xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel": "Niveau de sécurité du cloud", "xpack.csp.cloudPostureScoreChart.counterLink.failedFindingsTooltip": "Échec des résultats", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 31726bf596514..c795bbec6ed1f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10137,7 +10137,6 @@ "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " \"{name}\"", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "{pageCount}/{totalCount, plural, other {#個の統合}}を表示しています", "xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode}: {body}", - "xpack.csp.cloudPosturePage.packageNotInstalled.description": "{integrationFullName}(KSPM)統合は、CISの推奨事項に照らしてKubernetesクラスター設定を測定します。", "xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}", "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}", @@ -10168,8 +10167,6 @@ "xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel": "クラウドセキュリティ態勢", "xpack.csp.cloudPosturePage.errorRenderer.errorTitle": "クラウドセキュリティ態勢データを取得できませんでした", "xpack.csp.cloudPosturePage.loadingDescription": "読み込み中...", - "xpack.csp.cloudPosturePage.packageNotInstalled.buttonLabel": "KSPM統合の追加", - "xpack.csp.cloudPosturePage.packageNotInstalled.integrationNameLabel": "Kubernetesセキュリティ態勢管理", "xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle": "開始するには統合をインストールしてください", "xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel": "クラウドセキュリティ態勢", "xpack.csp.cloudPostureScoreChart.counterLink.failedFindingsTooltip": "失敗した調査結果", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0c43d9aecca7b..efc91d46c71bb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10152,7 +10152,6 @@ "xpack.csp.benchmarks.benchmarkEmptyState.integrationsNotFoundForNameTitle": " 对于“{name}”", "xpack.csp.benchmarks.totalIntegrationsCountMessage": "正在显示 {pageCount}/{totalCount, plural, other {# 个集成}}", "xpack.csp.cloudPosturePage.errorRenderer.errorDescription": "{error} {statusCode}:{body}", - "xpack.csp.cloudPosturePage.packageNotInstalled.description": "使用我们的 {integrationFullName} (KSPM) 集成根据 CIS 建议衡量 Kubernetes 集群设置。", "xpack.csp.complianceDashboard.complianceByCisSection.complianceColumnTooltip": "{passed}/{total}", "xpack.csp.dashboard.benchmarkSection.clusterTitle": "{title} - {shortId}", "xpack.csp.dashboard.benchmarkSection.clusterTitleTooltip.clusterTitle": "{title} - {shortId}", @@ -10183,8 +10182,6 @@ "xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel": "云安全态势", "xpack.csp.cloudPosturePage.errorRenderer.errorTitle": "我们无法提取您的云安全态势数据", "xpack.csp.cloudPosturePage.loadingDescription": "正在加载……", - "xpack.csp.cloudPosturePage.packageNotInstalled.buttonLabel": "添加 KSPM 集成", - "xpack.csp.cloudPosturePage.packageNotInstalled.integrationNameLabel": "Kubernetes 安全态势管理", "xpack.csp.cloudPosturePage.packageNotInstalled.pageTitle": "安装集成以开始", "xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel": "云安全态势", "xpack.csp.cloudPostureScoreChart.counterLink.failedFindingsTooltip": "失败的结果",