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": "失败的结果",