From a33e35399fbdde0a6b44549c60827e232cc5a23e Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Wed, 8 Feb 2023 17:57:54 +0000 Subject: [PATCH 01/19] initial groundwork for cloud_defend footprint in security manage page --- .../plugins/cloud_defend/common/constants.ts | 3 +- .../cloud_defend/common/schemas/policies.ts | 62 ++++ x-pack/plugins/cloud_defend/kibana.json | 2 +- .../cloud_defend/public/application/route.tsx | 49 +++ .../public/application/router.test.tsx | 101 +++++++ .../public/application/router.tsx | 52 ++++ .../application/security_solution_context.ts | 16 + .../public/common/navigation/constants.ts | 26 ++ .../public/common/navigation/query_utils.ts | 40 +++ .../security_solution_links.test.ts | 39 +++ .../navigation/security_solution_links.ts | 49 +++ .../public/common/navigation/types.ts | 23 ++ .../navigation/use_csp_integration_link.ts | 32 ++ ...se_navigate_to_cis_integration_policies.ts | 35 +++ .../public/components/loading_state/index.tsx | 23 ++ .../cloud_defend/public/pages/index.ts | 8 + .../public/pages/policies/benchmarks.test.tsx | 92 ++++++ .../pages/policies/benchmarks_table.test.tsx | 126 ++++++++ .../pages/policies/benchmarks_table.tsx | 200 ++++++++++++ .../public/pages/policies/index.ts | 8 + .../public/pages/policies/policies.tsx | 209 +++++++++++++ .../public/pages/policies/test_subjects.ts | 20 ++ .../use_csp_benchmark_integrations.ts | 46 +++ .../public/{plugin.ts => plugin.tsx} | 26 +- x-pack/plugins/cloud_defend/public/types.ts | 19 ++ x-pack/plugins/cloud_defend/server/index.ts | 18 ++ x-pack/plugins/cloud_defend/server/plugin.ts | 50 +++ .../cloud_defend/server/routes/index.ts | 24 ++ .../server/routes/policies/policies.test.ts | 285 ++++++++++++++++++ .../server/routes/policies/policies.ts | 122 ++++++++ .../server/routes/setup_routes.ts | 61 ++++ x-pack/plugins/cloud_defend/server/types.ts | 64 ++++ x-pack/plugins/cloud_defend/tsconfig.json | 14 +- .../public/cloud_defend/breadcrumbs.ts | 25 ++ .../public/cloud_defend/index.ts | 17 ++ .../public/cloud_defend/links.ts | 56 ++++ .../public/cloud_defend/routes.tsx | 54 ++++ .../public/management/links.ts | 6 + 38 files changed, 2086 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/cloud_defend/common/schemas/policies.ts create mode 100644 x-pack/plugins/cloud_defend/public/application/route.tsx create mode 100644 x-pack/plugins/cloud_defend/public/application/router.test.tsx create mode 100644 x-pack/plugins/cloud_defend/public/application/router.tsx create mode 100644 x-pack/plugins/cloud_defend/public/application/security_solution_context.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/constants.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/types.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/use_csp_integration_link.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx create mode 100644 x-pack/plugins/cloud_defend/public/pages/index.ts create mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/benchmarks.test.tsx create mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.test.tsx create mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx create mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/index.ts create mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/policies.tsx create mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts create mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/use_csp_benchmark_integrations.ts rename x-pack/plugins/cloud_defend/public/{plugin.ts => plugin.tsx} (58%) create mode 100644 x-pack/plugins/cloud_defend/server/index.ts create mode 100644 x-pack/plugins/cloud_defend/server/plugin.ts create mode 100644 x-pack/plugins/cloud_defend/server/routes/index.ts create mode 100644 x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts create mode 100644 x-pack/plugins/cloud_defend/server/routes/policies/policies.ts create mode 100644 x-pack/plugins/cloud_defend/server/routes/setup_routes.ts create mode 100644 x-pack/plugins/cloud_defend/server/types.ts create mode 100644 x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts create mode 100644 x-pack/plugins/security_solution/public/cloud_defend/index.ts create mode 100644 x-pack/plugins/security_solution/public/cloud_defend/links.ts create mode 100644 x-pack/plugins/security_solution/public/cloud_defend/routes.tsx diff --git a/x-pack/plugins/cloud_defend/common/constants.ts b/x-pack/plugins/cloud_defend/common/constants.ts index 860f3d4adffea..5adca73186186 100755 --- a/x-pack/plugins/cloud_defend/common/constants.ts +++ b/x-pack/plugins/cloud_defend/common/constants.ts @@ -6,7 +6,8 @@ */ export const PLUGIN_ID = 'cloudDefend'; -export const PLUGIN_NAME = 'cloudDefend'; +export const PLUGIN_NAME = 'Cloud Defend'; export const INTEGRATION_PACKAGE_NAME = 'cloud_defend'; export const INPUT_CONTROL = 'cloud_defend/control'; export const ALERTS_DATASET = 'cloud_defend.alerts'; +export const POLICIES_ROUTE_PATH = '/internal/cloud_defend/policies'; diff --git a/x-pack/plugins/cloud_defend/common/schemas/policies.ts b/x-pack/plugins/cloud_defend/common/schemas/policies.ts new file mode 100644 index 0000000000000..c6fff63b6bb81 --- /dev/null +++ b/x-pack/plugins/cloud_defend/common/schemas/policies.ts @@ -0,0 +1,62 @@ +/* + * 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 TypeOf, schema } from '@kbn/config-schema'; + +export const DEFAULT_POLICIES_PER_PAGE = 20; +export const POLICIES_PACKAGE_POLICY_PREFIX = 'package_policy.'; +export const policiesQueryParamsSchema = schema.object({ + /** + * The page of objects to return + */ + page: schema.number({ defaultValue: 1, min: 1 }), + /** + * The number of objects to include in each page + */ + per_page: schema.number({ defaultValue: DEFAULT_POLICIES_PER_PAGE, min: 0 }), + /** + * Once of PackagePolicy fields for sorting the found objects. + * Sortable fields: + * - package_policy.id + * - package_policy.name + * - package_policy.policy_id + * - package_policy.namespace + * - package_policy.updated_at + * - package_policy.updated_by + * - package_policy.created_at + * - package_policy.created_by, + * - package_policy.package.name + * - package_policy.package.title + * - package_policy.package.version + */ + sort_field: schema.oneOf( + [ + schema.literal('package_policy.id'), + schema.literal('package_policy.name'), + schema.literal('package_policy.policy_id'), + schema.literal('package_policy.namespace'), + schema.literal('package_policy.updated_at'), + schema.literal('package_policy.updated_by'), + schema.literal('package_policy.created_at'), + schema.literal('package_policy.created_by'), + schema.literal('package_policy.package.name'), + schema.literal('package_policy.package.title'), + ], + { defaultValue: 'package_policy.name' } + ), + /** + * The order to sort by + */ + sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], { + defaultValue: 'asc', + }), + /** + * Policy filter + */ + policy_name: schema.maybe(schema.string()), +}); + +export type PoliciesQueryParams = TypeOf; diff --git a/x-pack/plugins/cloud_defend/kibana.json b/x-pack/plugins/cloud_defend/kibana.json index af91d3eec34a0..336e20f4129ae 100755 --- a/x-pack/plugins/cloud_defend/kibana.json +++ b/x-pack/plugins/cloud_defend/kibana.json @@ -7,7 +7,7 @@ "githubTeam": "sec-cloudnative-integrations" }, "description": "Defend for Containers", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["fleet", "kibanaReact"], "optionalPlugins": [] diff --git a/x-pack/plugins/cloud_defend/public/application/route.tsx b/x-pack/plugins/cloud_defend/public/application/route.tsx new file mode 100644 index 0000000000000..5b35330bb961c --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/application/route.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, type RouteProps } from 'react-router-dom'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; +import { cloudDefendPages } from '../common/navigation/constants'; +import { useSecuritySolutionContext } from './security_solution_context'; +import type { CloudDefendPageNavigationItem } from '../common/navigation/types'; + +type CloudDefendRouteProps = Omit & CloudDefendPageNavigationItem; + +// Security SpyRoute can be automatically rendered for pages with static paths, Security will manage everything using the `links` object. +// Pages with dynamic paths are not in the Security `links` object, they must render SpyRoute with the parameters values, if needed. +const STATIC_PATH_PAGE_IDS = Object.fromEntries( + Object.values(cloudDefendPages).map(({ id }) => [id, true]) +); + +export const CloudDefendRoute: React.FC = ({ + id, + children, + component: Component, + disabled = false, + ...cloudDefendRouteProps +}) => { + const SpyRoute = useSecuritySolutionContext()?.getSpyRouteComponent(); + + if (disabled) { + return null; + } + + const routeProps: RouteProps = { + ...cloudDefendRouteProps, + ...(Component && { + render: (renderProps) => ( + + {STATIC_PATH_PAGE_IDS[id] && SpyRoute && } + + + ), + }), + }; + + return {children}; +}; diff --git a/x-pack/plugins/cloud_defend/public/application/router.test.tsx b/x-pack/plugins/cloud_defend/public/application/router.test.tsx new file mode 100644 index 0000000000000..a48a4943b3a5b --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/application/router.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 CloudDefendRouter from './router'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { Router } from 'react-router-dom'; +import type { CloudDefendPage, CloudDefendPageNavigationItem } from '../common/navigation/types'; +import { CloudDefendSecuritySolutionContext } from '../types'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import * as constants from '../common/navigation/constants'; +import { QueryClientProviderProps } from '@tanstack/react-query'; + +jest.mock('../pages', () => ({ + Policies: () =>
policies
, +})); + +jest.mock('@tanstack/react-query', () => ({ + QueryClientProvider: ({ children }: QueryClientProviderProps) => <>{children}, + QueryClient: jest.fn(), +})); + +describe('CloudDefendRouter', () => { + const originalCloudDefendPages = { ...constants.cloudDefendPages }; + const mockConstants = constants as { + cloudDefendPages: Record; + }; + + const securityContext: CloudDefendSecuritySolutionContext = { + getFiltersGlobalComponent: jest.fn(), + getSpyRouteComponent: () => () =>
, + }; + + let history: MemoryHistory; + + const renderCloudDefendRouter = () => + render( + + + + ); + + beforeEach(() => { + mockConstants.cloudDefendPages = originalCloudDefendPages; + jest.clearAllMocks(); + history = createMemoryHistory(); + }); + + describe('happy path', () => { + it('should render Policies', () => { + history.push('/cloud_defend/policies'); + const result = renderCloudDefendRouter(); + + expect(result.queryByTestId('Policies')).toBeInTheDocument(); + }); + }); + + describe('unhappy path', () => { + it('should redirect base path to policies', () => { + history.push('/cloud_defend/some_wrong_path'); + const result = renderCloudDefendRouter(); + + expect(history.location.pathname).toEqual('/cloud_defend/policies'); + expect(result.queryByTestId('Policies')).toBeInTheDocument(); + }); + }); + + describe('CloudDefendRoute', () => { + it('should not render disabled path', () => { + mockConstants.cloudDefendPages = { + ...constants.cloudDefendPages, + policies: { + ...constants.cloudDefendPages.policies, + disabled: true, + }, + }; + + history.push('/cloud_defend/policies'); + const result = renderCloudDefendRouter(); + + expect(result.queryByTestId('Policies')).not.toBeInTheDocument(); + }); + + it('should render SpyRoute for static paths', () => { + history.push('/cloud_defend/policies'); + const result = renderCloudDefendRouter(); + + expect(result.queryByTestId('mockedSpyRoute')).toBeInTheDocument(); + }); + + it('should not render SpyRoute for dynamic paths', () => { + history.push('/cloud_defend/policies/packagePolicyId/policyId/rules'); + const result = renderCloudDefendRouter(); + + expect(result.queryByTestId('mockedSpyRoute')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/cloud_defend/public/application/router.tsx b/x-pack/plugins/cloud_defend/public/application/router.tsx new file mode 100644 index 0000000000000..9796e20e39995 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/application/router.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { cloudDefendPages } from '../common/navigation/constants'; +import type { CloudDefendSecuritySolutionContext } from '../types'; +import { SecuritySolutionContext } from './security_solution_context'; +import * as pages from '../pages'; +import { CloudDefendRoute } from './route'; + +const queryClient = new QueryClient({ + defaultOptions: { queries: { refetchOnWindowFocus: false } }, +}); + +/** Props for the cloud security posture router component */ +export interface CloudDefendRouterProps { + securitySolutionContext?: CloudDefendSecuritySolutionContext; +} + +export const CloudDefendRouter = ({ securitySolutionContext }: CloudDefendRouterProps) => { + const routerElement = ( + + + + + + + + + + ); + + if (securitySolutionContext) { + return ( + + {routerElement} + + ); + } + + return <>{routerElement}; +}; + +// Using a default export for usage with `React.lazy` +// eslint-disable-next-line import/no-default-export +export { CloudDefendRouter as default }; diff --git a/x-pack/plugins/cloud_defend/public/application/security_solution_context.ts b/x-pack/plugins/cloud_defend/public/application/security_solution_context.ts new file mode 100644 index 0000000000000..fcbd10057021c --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/application/security_solution_context.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext } from 'react'; +import type { CloudDefendSecuritySolutionContext } from '../types'; + +export const SecuritySolutionContext = React.createContext< + CloudDefendSecuritySolutionContext | undefined +>(undefined); + +export const useSecuritySolutionContext = () => { + return useContext(SecuritySolutionContext); +}; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts b/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts new file mode 100644 index 0000000000000..e445398f69087 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts @@ -0,0 +1,26 @@ +/* + * 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 type { CloudDefendPage, CloudDefendPageNavigationItem } from './types'; + +const NAV_ITEMS_NAMES = { + POLICIES: i18n.translate('xpack.cloudDefend.navigation.policiesNavItemLabel', { + defaultMessage: 'Policies', + }), +}; + +/** The base path for all cloud defend pages. */ +export const CLOUD_DEFEND_BASE_PATH = '/cloud_defend'; + +export const cloudDefendPages: Record = { + policies: { + name: NAV_ITEMS_NAMES.POLICIES, + path: `${CLOUD_DEFEND_BASE_PATH}/policies`, + id: 'cloud_defend-policies', + }, +}; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts b/x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts new file mode 100644 index 0000000000000..3a051456733a6 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts @@ -0,0 +1,40 @@ +/* + * 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 { encode, decode } from '@kbn/rison'; +import type { LocationDescriptorObject } from 'history'; + +const encodeRison = (v: any): string | undefined => { + try { + return encode(v); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +}; + +const decodeRison = (query: string): T | undefined => { + try { + return decode(query) as T; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +}; + +const QUERY_PARAM_KEY = 'cspq'; + +export const encodeQuery = (query: any): LocationDescriptorObject['search'] => { + const risonQuery = encodeRison(query); + if (!risonQuery) return; + return `${QUERY_PARAM_KEY}=${risonQuery}`; +}; + +export const decodeQuery = (search?: string): Partial | undefined => { + const risonQuery = new URLSearchParams(search).get(QUERY_PARAM_KEY); + if (!risonQuery) return; + return decodeRison(risonQuery); +}; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts new file mode 100644 index 0000000000000..072776cde5a0c --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { cloudPosturePages } from './constants'; +import { getSecuritySolutionLink, getSecuritySolutionNavTab } from './security_solution_links'; +import { Chance } from 'chance'; +import type { CspPage } from './types'; + +const chance = new Chance(); + +describe('getSecuritySolutionLink', () => { + it('gets the correct link properties', () => { + const cspPage = chance.pickone(['dashboard', 'findings', 'benchmarks']); + + const link = getSecuritySolutionLink(cspPage); + + expect(link.id).toEqual(cloudPosturePages[cspPage].id); + expect(link.path).toEqual(cloudPosturePages[cspPage].path); + expect(link.title).toEqual(cloudPosturePages[cspPage].name); + }); +}); + +describe('getSecuritySolutionNavTab', () => { + it('gets the correct nav tab properties', () => { + const cspPage = chance.pickone(['dashboard', 'findings', 'benchmarks']); + const basePath = chance.word(); + + const navTab = getSecuritySolutionNavTab(cspPage, basePath); + + expect(navTab.id).toEqual(cloudPosturePages[cspPage].id); + expect(navTab.name).toEqual(cloudPosturePages[cspPage].name); + expect(navTab.href).toEqual(`${basePath}${cloudPosturePages[cspPage].path}`); + expect(navTab.disabled).toEqual(!!cloudPosturePages[cspPage].disabled); + }); +}); diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts new file mode 100644 index 0000000000000..9942c95f08094 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloudPosturePages } from './constants'; +import type { CloudSecurityPosturePageId, CspPage } from './types'; + +interface CloudSecurityPostureLinkItem { + id: TId; + title: string; + path: string; +} + +interface CloudSecurityPostureNavTab { + id: TId; + name: string; + href: string; + disabled: boolean; +} + +/** + * Gets the cloud security posture link properties of a CSP page for navigation in the security solution. + * @param cloudSecurityPosturePage the name of the cloud posture page. + */ +export const getSecuritySolutionLink = ( + cloudSecurityPosturePage: CspPage +): CloudSecurityPostureLinkItem => ({ + id: cloudPosturePages[cloudSecurityPosturePage].id as TId, + title: cloudPosturePages[cloudSecurityPosturePage].name, + path: cloudPosturePages[cloudSecurityPosturePage].path, +}); + +/** + * Gets the cloud security posture link properties of a CSP page for navigation in the old security solution navigation. + * @param cloudSecurityPosturePage the name of the cloud posture page. + * @param basePath the base path for links. + */ +export const getSecuritySolutionNavTab = ( + cloudSecurityPosturePage: CspPage, + basePath: string +): CloudSecurityPostureNavTab => ({ + id: cloudPosturePages[cloudSecurityPosturePage].id as TId, + name: cloudPosturePages[cloudSecurityPosturePage].name, + href: `${basePath}${cloudPosturePages[cloudSecurityPosturePage].path}`, + disabled: !!cloudPosturePages[cloudSecurityPosturePage].disabled, +}); diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/types.ts b/x-pack/plugins/cloud_defend/public/common/navigation/types.ts new file mode 100644 index 0000000000000..13530cae13985 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export interface CloudDefendNavigationItem { + readonly name: string; + readonly path: string; + readonly disabled?: boolean; +} + +export interface CloudDefendPageNavigationItem extends CloudDefendNavigationItem { + id: CloudDefendPageId; +} + +export type CloudDefendPage = 'policies'; + +/** + * All the IDs for the cloud defend pages. + * This needs to match the cloud defend page entries in `SecurityPageName` in `x-pack/plugins/security_solution/common/constants.ts`. + */ +export type CloudDefendPageId = 'cloud_security_posture-policies'; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/use_csp_integration_link.ts b/x-pack/plugins/cloud_defend/public/common/navigation/use_csp_integration_link.ts new file mode 100644 index 0000000000000..8d6e0f6c38583 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/use_csp_integration_link.ts @@ -0,0 +1,32 @@ +/* + * 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 { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public'; +import type { PosturePolicyTemplate } from '../../../common/types'; +import { useCisKubernetesIntegration } from '../api/use_cis_kubernetes_integration'; +import { useKibana } from '../hooks/use_kibana'; + +export const useCspIntegrationLink = ( + policyTemplate: PosturePolicyTemplate +): string | undefined => { + const { http } = useKibana().services; + const cisIntegration = useCisKubernetesIntegration(); + + if (!cisIntegration.isSuccess) return; + + const path = pagePathGetters + .add_integration_to_policy({ + integration: policyTemplate, + pkgkey: pkgKeyFromPackageInfo({ + name: cisIntegration.data.item.name, + version: cisIntegration.data.item.version, + }), + }) + .join(''); + + return http.basePath.prepend(path); +}; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.ts b/x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.ts new file mode 100644 index 0000000000000..350d7a8f38dac --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.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 { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public'; +import { useCisKubernetesIntegration } from '../api/use_cis_kubernetes_integration'; +import { useKibana } from '../hooks/use_kibana'; + +export const useCISIntegrationPoliciesLink = ({ + addAgentToPolicyId = '', + integration = '', +}: { + addAgentToPolicyId?: string; + integration?: string; +}): string | undefined => { + const { http } = useKibana().services; + const cisIntegration = useCisKubernetesIntegration(); + if (!cisIntegration.isSuccess) return; + + const path = pagePathGetters + .integration_details_policies({ + addAgentToPolicyId, + integration, + pkgkey: pkgKeyFromPackageInfo({ + name: cisIntegration.data.item.name, + version: cisIntegration.data.item.version, + }), + }) + .join(''); + + return http.basePath.prepend(path); +}; diff --git a/x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx b/x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx new file mode 100644 index 0000000000000..bee83f2d563b8 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx @@ -0,0 +1,23 @@ +/* + * 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 { EuiLoadingSpinner, EuiSpacer, EuiPageTemplate } from '@elastic/eui'; +import React from 'react'; + +// Keep this component lean as it is part of the main app bundle +export const LoadingState: React.FunctionComponent<{ ['data-test-subj']?: string }> = ({ + children, + ...rest +}) => { + return ( + + + + {children} + + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/pages/index.ts b/x-pack/plugins/cloud_defend/public/pages/index.ts new file mode 100644 index 0000000000000..ec35f87e70811 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Policies } from './policies'; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks.test.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks.test.tsx new file mode 100644 index 0000000000000..1d8b3d6e55a91 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 Chance from 'chance'; +import { render, screen } from '@testing-library/react'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { createCspBenchmarkIntegrationFixture } from '../../test/fixtures/csp_benchmark_integration'; +import { createReactQueryResponse } from '../../test/fixtures/react_query'; +import { TestProvider } from '../../test/test_provider'; +import { Benchmarks } from './benchmarks'; +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 { 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_csp_integration_link'); + +const chance = new Chance(); + +describe('', () => { + beforeEach(() => { + jest.resetAllMocks(); + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed' }, + }) + ); + + (useSubscriptionStatus as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: true, + }) + ); + + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + }); + + const renderBenchmarks = ( + queryResponse: Partial = createReactQueryResponse() + ) => { + (useCspBenchmarkIntegrations as jest.Mock).mockImplementation(() => queryResponse); + + return render( + + + + ); + }; + + it('renders the page header', () => { + renderBenchmarks(); + + expect(screen.getByTestId(TEST_SUBJ.BENCHMARKS_PAGE_HEADER)).toBeInTheDocument(); + }); + + it('renders the "add integration" button', () => { + renderBenchmarks(); + + expect(screen.getByTestId(TEST_SUBJ.ADD_INTEGRATION_TEST_SUBJ)).toBeInTheDocument(); + }); + + it('renders error state while there is an error', () => { + const error = new Error('message'); + renderBenchmarks(createReactQueryResponse({ status: 'error', error })); + + expect(screen.getByText(error.message)).toBeInTheDocument(); + }); + + it('renders the benchmarks table', () => { + renderBenchmarks( + createReactQueryResponse({ + status: 'success', + data: { total: 1, items: [createCspBenchmarkIntegrationFixture()] }, + }) + ); + + expect(screen.getByTestId(TEST_SUBJ.BENCHMARKS_TABLE_DATA_TEST_SUBJ)).toBeInTheDocument(); + Object.values(TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS).forEach((testId) => + expect(screen.getAllByTestId(testId)[0]).toBeInTheDocument() + ); + }); +}); diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.test.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.test.tsx new file mode 100644 index 0000000000000..dc980df0d67ae --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.test.tsx @@ -0,0 +1,126 @@ +/* + * 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 Chance from 'chance'; +import { render, screen } from '@testing-library/react'; +import moment from 'moment'; +import { createCspBenchmarkIntegrationFixture } from '../../test/fixtures/csp_benchmark_integration'; +import { BenchmarksTable } from './benchmarks_table'; +import { TestProvider } from '../../test/test_provider'; + +describe('', () => { + const chance = new Chance(); + + const tableProps = { + pageIndex: 1, + pageSize: 10, + error: undefined, + loading: false, + setQuery: jest.fn(), + }; + + it('renders integration name', () => { + const item = createCspBenchmarkIntegrationFixture(); + const benchmarks = [item]; + + render( + + + + ); + + expect(screen.getByText(item.package_policy.name)).toBeInTheDocument(); + }); + + it('renders agent policy name', () => { + const agentPolicy = { + id: chance.guid(), + name: chance.sentence(), + agents: chance.integer({ min: 1 }), + }; + + const benchmarks = [createCspBenchmarkIntegrationFixture({ agent_policy: agentPolicy })]; + + render( + + + + ); + + expect(screen.getByText(agentPolicy.name)).toBeInTheDocument(); + }); + + it('renders number of agents', () => { + const item = createCspBenchmarkIntegrationFixture(); + const benchmarks = [item]; + + render( + + + + ); + + // TODO too loose + expect(screen.getByText(item.agent_policy.agents as number)).toBeInTheDocument(); + }); + + it('renders created by', () => { + const item = createCspBenchmarkIntegrationFixture(); + const benchmarks = [item]; + + render( + + + + ); + + expect(screen.getByText(item.package_policy.created_by)).toBeInTheDocument(); + }); + + it('renders created at', () => { + const item = createCspBenchmarkIntegrationFixture(); + const benchmarks = [item]; + + render( + + + + ); + + expect(screen.getByText(moment(item.package_policy.created_at).fromNow())).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx new file mode 100644 index 0000000000000..ed276dc374744 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx @@ -0,0 +1,200 @@ +/* + * 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 { + EuiBasicTable, + type EuiBasicTableColumn, + type EuiBasicTableProps, + type Pagination, + type CriteriaWithPagination, + EuiLink, +} from '@elastic/eui'; +import React from 'react'; +import { generatePath } from 'react-router-dom'; +import { pagePathGetters } from '@kbn/fleet-plugin/public'; +import { i18n } from '@kbn/i18n'; +import type { PackagePolicy } from '@kbn/fleet-plugin/common'; +import { TimestampTableCell } from '../../components/timestamp_table_cell'; +import type { Benchmark } from '../../../common/types'; +import { useKibana } from '../../common/hooks/use_kibana'; +import { benchmarksNavigation } from '../../common/navigation/constants'; +import * as TEST_SUBJ from './test_subjects'; +import { getEnabledCspIntegrationDetails } from '../../common/utils/get_enabled_csp_integration_details'; + +interface BenchmarksTableProps + extends Pick, 'loading' | 'error' | 'noItemsMessage' | 'sorting'>, + Pagination { + benchmarks: Benchmark[]; + setQuery(pagination: CriteriaWithPagination): void; + 'data-test-subj'?: string; +} + +const AgentPolicyButtonLink = ({ name, id: policyId }: { name: string; id: string }) => { + const { http } = useKibana().services; + const [fleetBase, path] = pagePathGetters.policy_details({ policyId }); + + return {name}; +}; + +const IntegrationButtonLink = ({ + packageName, + policyId, + packagePolicyId, +}: { + packageName: string; + packagePolicyId: string; + policyId: string; +}) => { + const { application } = useKibana().services; + + return ( + + {packageName} + + ); +}; + +const BENCHMARKS_TABLE_COLUMNS: Array> = [ + { + field: 'package_policy.name', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.integrationNameColumnTitle', { + defaultMessage: 'Integration Name', + }), + render: (packageName, benchmark) => ( + + ), + truncateText: true, + sortable: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.INTEGRATION_NAME, + }, + { + field: 'rules_count', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.rulesColumnTitle', { + defaultMessage: 'Rules', + }), + truncateText: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.RULES, + }, + { + field: 'package_policy', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + dataType: 'string', + truncateText: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.INTEGRATION, + render: (field: PackagePolicy) => { + const enabledIntegration = getEnabledCspIntegrationDetails(field); + return enabledIntegration?.integration?.shortName || ' '; + }, + }, + { + field: 'package_policy', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.monitoringColumnTitle', { + defaultMessage: 'Monitoring', + }), + dataType: 'string', + truncateText: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.MONITORING, + render: (field: PackagePolicy) => { + const enabledIntegration = getEnabledCspIntegrationDetails(field); + return enabledIntegration?.enabledIntegrationOption?.name || ' '; + }, + }, + { + field: 'agent_policy.name', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.agentPolicyColumnTitle', { + defaultMessage: 'Agent Policy', + }), + render: (name, benchmark) => ( + + ), + truncateText: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.AGENT_POLICY, + }, + { + field: 'agent_policy.agents', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.numberOfAgentsColumnTitle', { + defaultMessage: 'Number of Agents', + }), + truncateText: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.NUMBER_OF_AGENTS, + }, + { + field: 'package_policy.created_by', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.createdByColumnTitle', { + defaultMessage: 'Created by', + }), + dataType: 'string', + truncateText: true, + sortable: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.CREATED_BY, + }, + { + field: 'package_policy.created_at', + name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.createdAtColumnTitle', { + defaultMessage: 'Created at', + }), + dataType: 'date', + truncateText: true, + render: (timestamp: Benchmark['package_policy']['created_at']) => ( + + ), + sortable: true, + 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.CREATED_AT, + }, +]; + +export const BenchmarksTable = ({ + benchmarks, + pageIndex, + pageSize, + totalItemCount, + loading, + error, + setQuery, + noItemsMessage, + sorting, + ...rest +}: BenchmarksTableProps) => { + const pagination: Pagination = { + pageIndex: Math.max(pageIndex - 1, 0), + pageSize, + totalItemCount, + }; + + const onChange = ({ page, sort }: CriteriaWithPagination) => { + setQuery({ page: { ...page, index: page.index + 1 }, sort }); + }; + + return ( + [item.agent_policy.id, item.package_policy.id].join('/')} + pagination={pagination} + onChange={onChange} + tableLayout="fixed" + loading={loading} + noItemsMessage={noItemsMessage} + error={error} + sorting={sorting} + /> + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.ts b/x-pack/plugins/cloud_defend/public/pages/policies/index.ts new file mode 100644 index 0000000000000..ec35f87e70811 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Policies } from './policies'; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/policies.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/policies.tsx new file mode 100644 index 0000000000000..dc5b35a17241a --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/policies/policies.tsx @@ -0,0 +1,209 @@ +/* + * 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, { useState } from 'react'; +import { + EuiButton, + EuiFieldSearch, + EuiFieldSearchProps, + EuiFlexGroup, + EuiFlexItem, + 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 { pagePathGetters } from '@kbn/fleet-plugin/public'; +import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; +import { CloudPosturePageTitle } from '../../components/cloud_posture_page_title'; +import { CloudPosturePage } from '../../components/cloud_posture_page'; +import { BenchmarksTable } from './benchmarks_table'; +import { + useCspBenchmarkIntegrations, + UseCspBenchmarkIntegrationsProps, +} from './use_csp_benchmark_integrations'; +import { extractErrorMessage } from '../../../common/utils/helpers'; +import * as TEST_SUBJ from './test_subjects'; +import { LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY } from '../../common/constants'; +import { usePageSize } from '../../common/hooks/use_page_size'; +import { useKibana } from '../../common/hooks/use_kibana'; + +const SEARCH_DEBOUNCE_MS = 300; + +const AddCisIntegrationButton = () => { + const { http } = useKibana().services; + + const integrationsPath = pagePathGetters + .integrations_all({ + searchTerm: INTEGRATION_PACKAGE_NAME, + }) + .join(''); + + return ( + + + + ); +}; + +const BenchmarkEmptyState = ({ name }: { name: string }) => ( +
+ + { + + + + {name && ( + + )} + + + } + + + + + + + +
+); + +const TotalIntegrationsCount = ({ + pageCount, + totalCount, +}: Record<'pageCount' | 'totalCount', number>) => ( + + + + + +); + +const BenchmarkSearchField = ({ + onSearch, + isLoading, +}: Required>) => { + const [localValue, setLocalValue] = useState(''); + + useDebounce(() => onSearch(localValue), SEARCH_DEBOUNCE_MS, [localValue]); + + return ( + + + + + + ); +}; + +export const Benchmarks = () => { + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY); + const [query, setQuery] = useState({ + name: '', + page: 1, + perPage: pageSize, + sortField: 'package_policy.name', + sortOrder: 'asc', + }); + + const queryResult = useCspBenchmarkIntegrations(query); + const totalItemCount = queryResult.data?.total || 0; + + return ( + + + } + rightSideItems={[]} + bottomBorder + /> + + setQuery((current) => ({ ...current, name }))} + /> + + + + { + setPageSize(page.size); + setQuery((current) => ({ + ...current, + page: page.index, + perPage: page.size, + sortField: + (sort?.field as UseCspBenchmarkIntegrationsProps['sortField']) || current.sortField, + sortOrder: sort?.direction || current.sortOrder, + })); + }} + noItemsMessage={ + queryResult.isSuccess && !queryResult.data.total ? ( + + ) : undefined + } + /> + + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts b/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts new file mode 100644 index 0000000000000..3e75715abf32f --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.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. + */ + +export const BENCHMARKS_PAGE_HEADER = 'benchmarks-page-header'; +export const BENCHMARKS_TABLE_DATA_TEST_SUBJ = 'csp_benchmarks_table'; +export const ADD_INTEGRATION_TEST_SUBJ = 'csp_add_integration'; +export const BENCHMARKS_TABLE_COLUMNS = { + INTEGRATION_NAME: 'benchmarks-table-column-integration-name', + MONITORING: 'benchmarks-table-column-monitoring', + RULES: 'benchmarks-table-column-rules', + INTEGRATION: 'benchmarks-table-column-integration', + AGENT_POLICY: 'benchmarks-table-column-agent-policy', + NUMBER_OF_AGENTS: 'benchmarks-table-column-number-of-agents', + CREATED_BY: 'benchmarks-table-column-created-by', + CREATED_AT: 'benchmarks-table-column-created-at', +}; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/use_csp_benchmark_integrations.ts b/x-pack/plugins/cloud_defend/public/pages/policies/use_csp_benchmark_integrations.ts new file mode 100644 index 0000000000000..ccdc3650b2596 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/pages/policies/use_csp_benchmark_integrations.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 { useQuery } from '@tanstack/react-query'; +import type { ListResult } from '@kbn/fleet-plugin/common'; +import { BENCHMARKS_ROUTE_PATH } from '../../../common/constants'; +import type { BenchmarksQueryParams } from '../../../common/schemas/benchmark'; +import { useKibana } from '../../common/hooks/use_kibana'; +import type { Benchmark } from '../../../common/types'; + +const QUERY_KEY = 'csp_benchmark_integrations'; + +export interface UseCspBenchmarkIntegrationsProps { + name: string; + page: number; + perPage: number; + sortField: BenchmarksQueryParams['sort_field']; + sortOrder: BenchmarksQueryParams['sort_order']; +} + +export const useCspBenchmarkIntegrations = ({ + name, + perPage, + page, + sortField, + sortOrder, +}: UseCspBenchmarkIntegrationsProps) => { + const { http } = useKibana().services; + const query: BenchmarksQueryParams = { + benchmark_name: name, + per_page: perPage, + page, + sort_field: sortField, + sort_order: sortOrder, + }; + + return useQuery( + [QUERY_KEY, query], + () => http.get>(BENCHMARKS_ROUTE_PATH, { query }), + { keepPreviousData: true } + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/plugin.ts b/x-pack/plugins/cloud_defend/public/plugin.tsx similarity index 58% rename from x-pack/plugins/cloud_defend/public/plugin.ts rename to x-pack/plugins/cloud_defend/public/plugin.tsx index 5bbb1215e2270..b3db2dcc41ab4 100755 --- a/x-pack/plugins/cloud_defend/public/plugin.ts +++ b/x-pack/plugins/cloud_defend/public/plugin.tsx @@ -4,20 +4,31 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { lazy } from 'react'; -import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import React, { lazy, Suspense } from 'react'; +import type { CloudDefendRouterProps } from './application/router'; import { CloudDefendPluginSetup, CloudDefendPluginStart, CloudDefendPluginStartDeps, } from './types'; import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; +import { LoadingState } from './components/loading_state'; const LazyEditPolicy = lazy(() => import('./components/fleet_extensions/policy_extension_edit')); const LazyCreatePolicy = lazy( () => import('./components/fleet_extensions/policy_extension_create') ); +const RouterLazy = lazy(() => import('./application/router')); +const Router = (props: CloudDefendRouterProps) => ( + }> + + +); + export class CloudDefendPlugin implements Plugin { public setup(core: CoreSetup): CloudDefendPluginSetup { // Return methods that should be available to other plugins @@ -37,7 +48,16 @@ export class CloudDefendPlugin implements Plugin (props: CloudDefendRouterProps) => + ( + + + + + + ), + }; } public stop() {} diff --git a/x-pack/plugins/cloud_defend/public/types.ts b/x-pack/plugins/cloud_defend/public/types.ts index 3f1704b2cd07e..a4a94db0cd6d0 100755 --- a/x-pack/plugins/cloud_defend/public/types.ts +++ b/x-pack/plugins/cloud_defend/public/types.ts @@ -7,6 +7,12 @@ import type { FleetSetup, FleetStart } from '@kbn/fleet-plugin/public'; import { NewPackagePolicy } from '@kbn/fleet-plugin/public'; +import type { ComponentType, ReactNode } from 'react'; +import type { CloudDefendPageId } from './common/navigation/types'; + +/** + * cloud_defend plugin types + */ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CloudDefendPluginSetup {} @@ -20,6 +26,19 @@ export interface CloudDefendPluginStartDeps { fleet: FleetStart; } +export interface CloudDefendSecuritySolutionContext { + /** Gets the `FiltersGlobal` component for embedding a filter bar in the security solution application. */ + getFiltersGlobalComponent: () => ComponentType<{ children: ReactNode }>; + /** Gets the `SpyRoute` component for navigation highlighting and breadcrumbs. */ + getSpyRouteComponent: () => ComponentType<{ + pageName: CloudDefendPageId; + state?: Record; + }>; +} + +/** + * cloud_defend/control types + */ export enum ControlResponseAction { alert = 'alert', block = 'block', diff --git a/x-pack/plugins/cloud_defend/server/index.ts b/x-pack/plugins/cloud_defend/server/index.ts new file mode 100644 index 0000000000000..2cb2e1c2b55e5 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { CloudDefendPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new CloudDefendPlugin(initializerContext); +} + +export type { CloudDefendPluginSetup, CloudDefendPluginStart } from './types'; diff --git a/x-pack/plugins/cloud_defend/server/plugin.ts b/x-pack/plugins/cloud_defend/server/plugin.ts new file mode 100644 index 0000000000000..777d6a00becf0 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/plugin.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; +import { + CloudDefendPluginSetup, + CloudDefendPluginStart, + CloudDefendPluginStartDeps, +} from './types'; +import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; +import { setupRoutes } from './routes/setup_routes'; + +export class CloudDefendPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('cloudDefend: Setup'); + const router = core.http.createRouter(); + + setupRoutes({ + core, + logger: this.logger, + }); + + return {}; + } + + public start(core: CoreStart, plugins: CloudDefendPluginStartDeps): CloudDefendPluginStart { + this.logger.debug('cloudDefend: Started'); + + plugins.fleet.fleetSetupCompleted().then(async () => { + const packageInfo = await plugins.fleet.packageService.asInternalUser.getInstallation( + INTEGRATION_PACKAGE_NAME + ); + + console.log(packageInfo); + }); + + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/cloud_defend/server/routes/index.ts b/x-pack/plugins/cloud_defend/server/routes/index.ts new file mode 100644 index 0000000000000..1a5467577aa30 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/routes/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter, Logger } from '@kbn/core/server'; + +export function defineRoutes(router: IRouter, logger: Logger) { + router.get( + { + path: '/api/cloud_defend/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts b/x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts new file mode 100644 index 0000000000000..d8b432bb0391e --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts @@ -0,0 +1,285 @@ +/* + * 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 { httpServerMock, httpServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { + benchmarksQueryParamsSchema, + DEFAULT_BENCHMARKS_PER_PAGE, +} from '../../../common/schemas/benchmark'; +import { + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + getCspPackagePolicies, + getCspAgentPolicies, +} from '../../lib/fleet_util'; +import { defineGetBenchmarksRoute, getRulesCountForPolicy } from './benchmarks'; + +import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; +import { + createMockAgentPolicyService, + createPackagePolicyServiceMock, +} from '@kbn/fleet-plugin/server/mocks'; +import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; +import { createCspRequestHandlerContextMock } from '../../mocks'; + +describe('benchmarks API', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('validate the API route path', async () => { + const router = httpServiceMock.createRouter(); + + defineGetBenchmarksRoute(router); + + const [config] = router.get.mock.calls[0]; + + expect(config.path).toEqual('/internal/cloud_security_posture/benchmarks'); + }); + + it('should accept to a user with fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + + defineGetBenchmarksRoute(router); + + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = createCspRequestHandlerContextMock(); + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(0); + }); + + it('should reject to a user without fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + + defineGetBenchmarksRoute(router); + + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = createCspRequestHandlerContextMock(); + mockContext.fleet.authz.fleet.all = false; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(1); + }); + + describe('test input schema', () => { + it('expect to find default values', async () => { + const validatedQuery = benchmarksQueryParamsSchema.validate({}); + + expect(validatedQuery).toMatchObject({ + page: 1, + per_page: DEFAULT_BENCHMARKS_PER_PAGE, + }); + }); + + it('expect to find benchmark_name', async () => { + const validatedQuery = benchmarksQueryParamsSchema.validate({ + benchmark_name: 'my_cis_benchmark', + }); + + expect(validatedQuery).toMatchObject({ + page: 1, + per_page: DEFAULT_BENCHMARKS_PER_PAGE, + benchmark_name: 'my_cis_benchmark', + }); + }); + + it('should throw when page field is not a positive integer', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ page: -2 }); + }).toThrow(); + }); + + it('should throw when per_page field is not a positive integer', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ per_page: -2 }); + }).toThrow(); + }); + }); + + it('should throw when sort_field is not string', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ sort_field: true }); + }).toThrow(); + }); + + it('should not throw when sort_field is a string', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ sort_field: 'package_policy.name' }); + }).not.toThrow(); + }); + + it('should throw when sort_order is not `asc` or `desc`', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ sort_order: 'Other Direction' }); + }).toThrow(); + }); + + it('should not throw when `asc` is input for sort_order field', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ sort_order: 'asc' }); + }).not.toThrow(); + }); + + it('should not throw when `desc` is input for sort_order field', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ sort_order: 'desc' }); + }).not.toThrow(); + }); + + it('should not throw when fields is a known string literal', async () => { + expect(() => { + benchmarksQueryParamsSchema.validate({ sort_field: 'package_policy.name' }); + }).not.toThrow(); + }); + + describe('test benchmarks utils', () => { + let mockSoClient: jest.Mocked; + + beforeEach(() => { + mockSoClient = savedObjectsClientMock.create(); + }); + + describe('test getPackagePolicies', () => { + it('should format request by package name', async () => { + const mockPackagePolicyService = createPackagePolicyServiceMock(); + + await getCspPackagePolicies(mockSoClient, mockPackagePolicyService, 'myPackage', { + page: 1, + per_page: 100, + sort_order: 'desc', + }); + + expect(mockPackagePolicyService.list.mock.calls[0][1]).toMatchObject( + expect.objectContaining({ + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage`, + page: 1, + perPage: 100, + }) + ); + }); + + it('should build sort request by `sort_field` and default `sort_order`', async () => { + const mockAgentPolicyService = createPackagePolicyServiceMock(); + + await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + page: 1, + per_page: 100, + sort_field: 'package_policy.name', + sort_order: 'desc', + }); + + expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( + expect.objectContaining({ + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage`, + page: 1, + perPage: 100, + sortField: 'name', + sortOrder: 'desc', + }) + ); + }); + + it('should build sort request by `sort_field` and asc `sort_order`', async () => { + const mockAgentPolicyService = createPackagePolicyServiceMock(); + + await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + page: 1, + per_page: 100, + sort_field: 'package_policy.name', + sort_order: 'asc', + }); + + expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( + expect.objectContaining({ + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage`, + page: 1, + perPage: 100, + sortField: 'name', + sortOrder: 'asc', + }) + ); + }); + }); + + it('should format request by benchmark_name', async () => { + const mockAgentPolicyService = createPackagePolicyServiceMock(); + + await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + page: 1, + per_page: 100, + sort_order: 'desc', + benchmark_name: 'my_cis_benchmark', + }); + + expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( + expect.objectContaining({ + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *my_cis_benchmark*`, + page: 1, + perPage: 100, + }) + ); + }); + + describe('test getAgentPolicies', () => { + it('should return one agent policy id when there is duplication', async () => { + const agentPolicyService = createMockAgentPolicyService(); + const packagePolicies = [createPackagePolicyMock(), createPackagePolicyMock()]; + + await getCspAgentPolicies(mockSoClient, packagePolicies, agentPolicyService); + + expect(agentPolicyService.getByIds.mock.calls[0][1]).toHaveLength(1); + }); + + it('should return full policy ids list when there is no id duplication', async () => { + const agentPolicyService = createMockAgentPolicyService(); + + const packagePolicy1 = createPackagePolicyMock(); + const packagePolicy2 = createPackagePolicyMock(); + packagePolicy2.policy_id = 'AnotherId'; + const packagePolicies = [packagePolicy1, packagePolicy2]; + + await getCspAgentPolicies(mockSoClient, packagePolicies, agentPolicyService); + + expect(agentPolicyService.getByIds.mock.calls[0][1]).toHaveLength(2); + }); + }); + + describe('test addPackagePolicyCspRuleTemplates', () => { + it('should retrieve the rules count by the filtered benchmark type', async () => { + const benchmark = 'cis_k8s'; + mockSoClient.find.mockResolvedValueOnce({ + aggregations: { enabled_status: { doc_count: 2 } }, + page: 1, + per_page: 10000, + total: 3, + saved_objects: [ + { + type: 'csp_rule', + id: '0af387d0-c933-11ec-b6c8-4f8afc058cc3', + }, + ], + } as unknown as SavedObjectsFindResponse); + + const rulesCount = await getRulesCountForPolicy(mockSoClient, benchmark); + + const expectedFilter = `csp-rule-template.attributes.metadata.benchmark.id: "${benchmark}"`; + expect(mockSoClient.find.mock.calls[0][0].filter).toEqual(expectedFilter); + expect(rulesCount).toEqual(3); + }); + }); + }); +}); diff --git a/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts b/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts new file mode 100644 index 0000000000000..f4a081cf00165 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts @@ -0,0 +1,122 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { AgentPolicy, PackagePolicy } from '@kbn/fleet-plugin/common'; +import { POLICIES_ROUTE_PATH, INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; +import { policiesQueryParamsSchema } from '../../../common/schemas/benchmark'; +import type { Benchmark } from '../../../common/types'; +import { + getBenchmarkFromPackagePolicy, + getBenchmarkTypeFilter, + isNonNullable, +} from '../../../common/utils/helpers'; +import { CloudDefendRouter } from '../../types'; +import { + getAgentStatusesByAgentPolicies, + type AgentStatusByAgentPolicyMap, + getCloudDefendAgentPolicies, + getCloudDefendPackagePolicies, +} from '../../lib/fleet_util'; + +export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; + +const createPolicies = ( + agentPolicies: AgentPolicy[], + agentStatusByAgentPolicyId: AgentStatusByAgentPolicyMap, + cspPackagePolicies: PackagePolicy[] +): Promise => { + const cspPackagePoliciesMap = new Map( + cspPackagePolicies.map((packagePolicy) => [packagePolicy.id, packagePolicy]) + ); + + return Promise.all( + agentPolicies.flatMap((agentPolicy) => { + const cspPackagesOnAgent = + agentPolicy.package_policies + ?.map(({ id: pckPolicyId }) => { + return cspPackagePoliciesMap.get(pckPolicyId); + }) + .filter(isNonNullable) ?? []; + + const benchmarks = cspPackagesOnAgent.map(async (cspPackage) => { + const benchmarkId = getBenchmarkFromPackagePolicy(cspPackage.inputs); + const agentPolicyStatus = { + id: agentPolicy.id, + name: agentPolicy.name, + agents: agentStatusByAgentPolicyId[agentPolicy.id]?.total, + }; + return { + package_policy: cspPackage, + agent_policy: agentPolicyStatus, + }; + }); + + return benchmarks; + }) + ); +}; + +export const defineGetPoliciesRoute = (router: CloudDefendRouter): void => + router.get( + { + path: POLICIES_ROUTE_PATH, + validate: { query: policiesQueryParamsSchema }, + options: { + tags: ['access:cloud-defend-read'], + }, + }, + async (context, request, response) => { + if (!(await context.fleet).authz.fleet.all) { + return response.forbidden(); + } + + const cloudDefendContext = await context.cloudDefend; + + try { + const cloudDefendPackagePolicies = await getCloudDefendPackagePolicies( + cloudDefendContext.soClient, + cloudDefendContext.packagePolicyService, + INTEGRATION_PACKAGE_NAME, + request.query + ); + + const agentPolicies = await getCloudDefendAgentPolicies( + cloudDefendContext.soClient, + cloudDefendPackagePolicies.items, + cloudDefendContext.agentPolicyService + ); + + const agentStatusesByAgentPolicyId = await getAgentStatusesByAgentPolicies( + cloudDefendContext.agentService, + agentPolicies, + cloudDefendContext.logger + ); + + const policies = await createPolicies( + cloudDefendContext.soClient, + agentPolicies, + agentStatusesByAgentPolicyId, + cloudDefendPackagePolicies.items + ); + + return response.ok({ + body: { + ...cloudDefendPackagePolicies, + items: policies, + }, + }); + } catch (err) { + const error = transformError(err); + cloudDefendContext.logger.error(`Failed to fetch policies ${err}`); + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode, + }); + } + } + ); diff --git a/x-pack/plugins/cloud_defend/server/routes/setup_routes.ts b/x-pack/plugins/cloud_defend/server/routes/setup_routes.ts new file mode 100644 index 0000000000000..8e9f3e4f0a637 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/routes/setup_routes.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, Logger } from '@kbn/core/server'; +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import type { + CloudDefendRequestHandlerContext, + CloudDefendPluginStart, + CloudDefendPluginStartDeps, +} from '../types'; +import { PLUGIN_ID } from '../../common/constants'; +import { defineGetPoliciesRoute } from './policies/policies'; +// import { defineGetCloudDefendStatusRoute } from './status/status'; + +/** + * 1. Registers routes + * 2. Registers routes handler context + */ +export function setupRoutes({ + core, + logger, +}: { + core: CoreSetup; + logger: Logger; +}) { + const router = core.http.createRouter(); + defineGetPoliciesRoute(router); + // defineGetCloudDefendStatusRoute(router); + + core.http.registerRouteHandlerContext( + PLUGIN_ID, + async (context, request) => { + const [, { security, fleet }] = await core.getStartServices(); + const coreContext = await context.core; + await fleet.fleetSetupCompleted(); + + let user: AuthenticatedUser | null = null; + + return { + get user() { + // We want to call getCurrentUser only when needed and only once + if (!user) { + user = security.authc.getCurrentUser(request); + } + return user; + }, + logger, + esClient: coreContext.elasticsearch.client, + soClient: coreContext.savedObjects.client, + agentPolicyService: fleet.agentPolicyService, + agentService: fleet.agentService, + packagePolicyService: fleet.packagePolicyService, + packageService: fleet.packageService, + }; + } + ); +} diff --git a/x-pack/plugins/cloud_defend/server/types.ts b/x-pack/plugins/cloud_defend/server/types.ts new file mode 100644 index 0000000000000..2d3431fcfdb5b --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/types.ts @@ -0,0 +1,64 @@ +/* + * 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 { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import type { + IRouter, + CustomRequestHandlerContext, + Logger, + SavedObjectsClientContract, + IScopedClusterClient, +} from '@kbn/core/server'; +import type { + FleetStartContract, + FleetRequestHandlerContext, + AgentService, + PackageService, + AgentPolicyServiceInterface, + PackagePolicyClient, +} from '@kbn/fleet-plugin/server'; +import type { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '@kbn/data-plugin/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CloudDefendPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CloudDefendPluginStart {} + +export interface CloudDefendPluginSetupDeps { + data: DataPluginSetup; + security: SecurityPluginSetup; +} +export interface CloudDefendPluginStartDeps { + data: DataPluginStart; + fleet: FleetStartContract; + security: SecurityPluginStart; +} + +export interface CloudDefendApiRequestHandlerContext { + user: ReturnType; + logger: Logger; + esClient: IScopedClusterClient; + soClient: SavedObjectsClientContract; + agentPolicyService: AgentPolicyServiceInterface; + agentService: AgentService; + packagePolicyService: PackagePolicyClient; + packageService: PackageService; + isPluginInitialized(): boolean; +} + +export type CloudDefendRequestHandlerContext = CustomRequestHandlerContext<{ + cloudDefend: CloudDefendApiRequestHandlerContext; + fleet: FleetRequestHandlerContext['fleet']; +}>; + +/** + * Convenience type for routers in Csp that includes the CspRequestHandlerContext type + * @internal + */ +export type CloudDefendRouter = IRouter; diff --git a/x-pack/plugins/cloud_defend/tsconfig.json b/x-pack/plugins/cloud_defend/tsconfig.json index aabb3ece1e8da..2515fd96e95bc 100755 --- a/x-pack/plugins/cloud_defend/tsconfig.json +++ b/x-pack/plugins/cloud_defend/tsconfig.json @@ -1,25 +1,17 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "target/types", + "outDir": "target/types" }, - "include": [ - "common/**/*", - "public/**/*", - "../../../typings/**/*" - ], + "include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*"], "kbn_references": [ "@kbn/core", "@kbn/fleet-plugin", - "@kbn/fleet-plugin", - "@kbn/core", "@kbn/i18n-react", "@kbn/data-plugin", "@kbn/kibana-react-plugin", "@kbn/monaco", "@kbn/i18n" ], - "exclude": [ - "target/**/*", - ] + "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts b/x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts new file mode 100644 index 0000000000000..16bd30db6680d --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts @@ -0,0 +1,25 @@ +/* + * 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 { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; +import type { GetSecuritySolutionUrl } from '../common/components/link_to'; +import type { RouteSpyState } from '../common/utils/route/types'; + +export const getTrailingBreadcrumbs = ( + params: RouteSpyState, + getSecuritySolutionUrl: GetSecuritySolutionUrl +): ChromeBreadcrumb[] => { + const breadcrumbs = []; + + if (params.state?.ruleName) { + breadcrumbs.push({ + text: params.state.ruleName, + }); + } + + return breadcrumbs; +}; diff --git a/x-pack/plugins/security_solution/public/cloud_defend/index.ts b/x-pack/plugins/security_solution/public/cloud_defend/index.ts new file mode 100644 index 0000000000000..798658b15cd42 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_defend/index.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 { SecuritySubPlugin } from '../app/types'; +import { routes } from './routes'; + +export class CloudSecurityPosture { + public setup() {} + + public start(): SecuritySubPlugin { + return { routes }; + } +} diff --git a/x-pack/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/plugins/security_solution/public/cloud_defend/links.ts new file mode 100644 index 0000000000000..def3b0ed9f5eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_defend/links.ts @@ -0,0 +1,56 @@ +/* + * 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 { getSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import cloudSecurityPostureDashboardImage from '../common/images/cloud_security_posture_dashboard_page.png'; +import type { LinkCategories, LinkItem } from '../common/links/types'; +import { IconExceptionLists } from '../management/icons/exception_lists'; + +const commonLinkProperties: Partial = { + hideTimeline: true, + capabilities: [`${SERVER_APP_ID}.show`], +}; + +export const rootLinks: LinkItem = { + ...getSecuritySolutionLink('findings'), + globalNavPosition: 3, + ...commonLinkProperties, +}; + +export const dashboardLinks: LinkItem = { + ...getSecuritySolutionLink('dashboard'), + description: i18n.translate( + 'xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription', + { + defaultMessage: 'An overview of findings across all CSP integrations.', + } + ), + landingImage: cloudSecurityPostureDashboardImage, + ...commonLinkProperties, +}; + +export const manageLinks: LinkItem = { + ...getSecuritySolutionLink('benchmarks'), + description: i18n.translate( + 'xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription', + { + defaultMessage: 'View benchmark rules.', + } + ), + landingIcon: IconExceptionLists, + ...commonLinkProperties, +}; + +export const manageCategories: LinkCategories = [ + { + label: i18n.translate('xpack.securitySolution.appLinks.category.cloudSecurityPosture', { + defaultMessage: 'CLOUD SECURITY POSTURE', + }), + linkIds: [SecurityPageName.cloudSecurityPostureBenchmarks], + }, +]; diff --git a/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx b/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx new file mode 100644 index 0000000000000..f11bac97dfcf0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx @@ -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 React from 'react'; +import type { CloudSecurityPosturePageId } from '@kbn/cloud-security-posture-plugin/public'; +import { + CLOUD_SECURITY_POSTURE_BASE_PATH, + type CspSecuritySolutionContext, +} from '@kbn/cloud-security-posture-plugin/public'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; +import type { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; +import { useKibana } from '../common/lib/kibana'; +import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper'; +import { SpyRoute } from '../common/utils/route/spy_routes'; +import { FiltersGlobal } from '../common/components/filters_global'; +import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; + +// This exists only for the type signature cast +const CloudPostureSpyRoute = ({ pageName, ...rest }: { pageName?: CloudSecurityPosturePageId }) => ( + +); + +const cspSecuritySolutionContext: CspSecuritySolutionContext = { + getFiltersGlobalComponent: () => FiltersGlobal, + getSpyRouteComponent: () => CloudPostureSpyRoute, +}; + +const CloudSecurityPosture = () => { + const { cloudSecurityPosture } = useKibana().services; + const CloudSecurityPostureRouter = cloudSecurityPosture.getCloudSecurityPostureRouter(); + + return ( + + + + + + + + ); +}; + +CloudSecurityPosture.displayName = 'CloudSecurityPosture'; + +export const routes: SecuritySubPluginRoutes = [ + { + path: CLOUD_SECURITY_POSTURE_BASE_PATH, + component: CloudSecurityPosture, + }, +]; diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 1db32eae3d5f2..911b0e0644824 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -49,6 +49,10 @@ import { manageCategories as cloudSecurityPostureCategories, manageLinks as cloudSecurityPostureLinks, } from '../cloud_security_posture/links'; +import { + manageCategories as cloudDefendCategories, + manageLinks as cloudDefendLinks, +} from '../cloud_defend/links'; import { IconActionHistory } from './icons/action_history'; import { IconBlocklist } from './icons/blocklist'; import { IconEndpoints } from './icons/endpoints'; @@ -83,6 +87,7 @@ const categories = [ ], }, ...cloudSecurityPostureCategories, + ...cloudDefendCategories, ]; export const links: LinkItem = { @@ -226,6 +231,7 @@ export const links: LinkItem = { hideTimeline: true, }, cloudSecurityPostureLinks, + cloudDefendLinks, ], }; From c417d41e4ab8b6a0c4f771b85ce8abc9548b52d5 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Fri, 10 Feb 2023 02:42:50 +0000 Subject: [PATCH 02/19] boilerplate fun --- .github/CODEOWNERS | 5 +- .../common/schemas/{policies.ts => policy.ts} | 0 x-pack/plugins/cloud_defend/common/types.ts | 57 +++ .../cloud_defend/common/utils/helpers.test.ts | 63 ++++ .../cloud_defend/common/utils/helpers.ts | 68 ++++ .../common/utils/subscription.test.ts | 44 +++ .../cloud_defend/common/utils/subscription.ts | 24 ++ x-pack/plugins/cloud_defend/kibana.json | 2 +- .../public/application/router.tsx | 4 +- .../api/use_cloud_defend_integration.tsx | 26 ++ .../public/common/api/use_setup_status_api.ts | 24 ++ .../cloud_defend/public/common/constants.ts | 2 + .../public/common/hooks/use_kibana.ts | 14 + .../public/common/hooks/use_page_size.ts | 26 ++ .../navigation/security_solution_links.ts | 40 +- .../public/common/navigation/types.ts | 2 +- ...s => use_cloud_defend_integration_link.ts} | 18 +- ...se_navigate_to_cis_integration_policies.ts | 35 -- .../cloud_defend_page/index.test.tsx | 357 ++++++++++++++++++ .../components/cloud_defend_page/index.tsx | 300 +++++++++++++++ .../cloud_defend_page_title/index.tsx | 19 + .../components/full_size_page/index.tsx | 28 ++ .../policies_table/index.test.tsx} | 0 .../components/policies_table/index.tsx | 172 +++++++++ .../components/timestamp_table_cell/index.tsx | 24 ++ x-pack/plugins/cloud_defend/public/index.ts | 5 + .../pages/policies/benchmarks_table.tsx | 200 ---------- .../{benchmarks.test.tsx => index.test.tsx} | 0 .../public/pages/policies/index.ts | 8 - .../policies/{policies.tsx => index.tsx} | 57 ++- .../public/pages/policies/test_subjects.ts | 24 +- ...ations.ts => use_cloud_defend_policies.ts} | 24 +- x-pack/plugins/cloud_defend/public/plugin.tsx | 4 +- x-pack/plugins/cloud_defend/public/types.ts | 13 +- .../cloud_defend/server/lib/fleet_util.ts | 121 ++++++ .../server/routes/policies/policies.ts | 32 +- x-pack/plugins/cloud_defend/server/types.ts | 2 +- x-pack/plugins/cloud_defend/tsconfig.json | 3 +- .../security_solution/common/constants.ts | 5 + x-pack/plugins/security_solution/kibana.json | 1 + .../public/app/deep_links/index.ts | 12 +- .../public/cloud_defend/index.ts | 2 +- .../public/cloud_defend/links.ts | 40 +- .../public/cloud_defend/routes.tsx | 32 +- .../common/components/navigation/types.ts | 2 + .../plugins/security_solution/public/types.ts | 5 + .../plugins/security_solution/tsconfig.json | 1 + 47 files changed, 1539 insertions(+), 408 deletions(-) rename x-pack/plugins/cloud_defend/common/schemas/{policies.ts => policy.ts} (100%) create mode 100644 x-pack/plugins/cloud_defend/common/types.ts create mode 100644 x-pack/plugins/cloud_defend/common/utils/helpers.test.ts create mode 100644 x-pack/plugins/cloud_defend/common/utils/helpers.ts create mode 100644 x-pack/plugins/cloud_defend/common/utils/subscription.test.ts create mode 100644 x-pack/plugins/cloud_defend/common/utils/subscription.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/api/use_cloud_defend_integration.tsx create mode 100644 x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/hooks/use_kibana.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/hooks/use_page_size.ts rename x-pack/plugins/cloud_defend/public/common/navigation/{use_csp_integration_link.ts => use_cloud_defend_integration_link.ts} (55%) delete mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx create mode 100644 x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx create mode 100644 x-pack/plugins/cloud_defend/public/components/cloud_defend_page_title/index.tsx create mode 100644 x-pack/plugins/cloud_defend/public/components/full_size_page/index.tsx rename x-pack/plugins/cloud_defend/public/{pages/policies/benchmarks_table.test.tsx => components/policies_table/index.test.tsx} (100%) create mode 100644 x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx create mode 100644 x-pack/plugins/cloud_defend/public/components/timestamp_table_cell/index.tsx delete mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx rename x-pack/plugins/cloud_defend/public/pages/policies/{benchmarks.test.tsx => index.test.tsx} (100%) delete mode 100644 x-pack/plugins/cloud_defend/public/pages/policies/index.ts rename x-pack/plugins/cloud_defend/public/pages/policies/{policies.tsx => index.tsx} (77%) rename x-pack/plugins/cloud_defend/public/pages/policies/{use_csp_benchmark_integrations.ts => use_cloud_defend_policies.ts} (54%) create mode 100644 x-pack/plugins/cloud_defend/server/lib/fleet_util.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e600849f8095e..2e388c3862edd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -644,13 +644,16 @@ x-pack/test/threat_intelligence_cypress @elastic/protections-experience /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 Defend +/x-pack/plugins/cloud_defend/ @elastic/sec-cloudnative-integrations +/x-pack/plugins/security_solution/public/cloud_defend @elastic/sec-cloudnative-integrations + # Cloud Security Posture /x-pack/plugins/cloud_security_posture/ @elastic/kibana-cloud-security-posture /x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture /x-pack/test/api_integration/apis/cloud_security_posture/ @elastic/kibana-cloud-security-posture /x-pack/test/cloud_security_posture_functional/ @elastic/kibana-cloud-security-posture - # Security Solution onboarding tour /x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore /x-pack/plugins/security_solution/cypress/e2e/guided_onboarding @elastic/security-threat-hunting-explore diff --git a/x-pack/plugins/cloud_defend/common/schemas/policies.ts b/x-pack/plugins/cloud_defend/common/schemas/policy.ts similarity index 100% rename from x-pack/plugins/cloud_defend/common/schemas/policies.ts rename to x-pack/plugins/cloud_defend/common/schemas/policy.ts diff --git a/x-pack/plugins/cloud_defend/common/types.ts b/x-pack/plugins/cloud_defend/common/types.ts new file mode 100644 index 0000000000000..8d3abad0ab202 --- /dev/null +++ b/x-pack/plugins/cloud_defend/common/types.ts @@ -0,0 +1,57 @@ +/* + * 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 { PackagePolicy, AgentPolicy } from '@kbn/fleet-plugin/common'; + +export type IndexStatus = + | 'not-empty' // Index contains documents + | 'empty' // Index doesn't contain documents (or doesn't exist) + | 'unprivileged'; // User doesn't have access to query the index + +export type CloudDefendStatusCode = + | 'indexed' // latest findings index exists and has results + | 'indexing' // index timeout was not surpassed since installation, assumes data is being indexed + | 'unprivileged' // user lacks privileges for the latest findings index + | 'index-timeout' // index timeout was surpassed since installation + | 'not-deployed' // no healthy agents were deployed + | 'not-installed'; // number of installed csp integrations is 0; + +export interface IndexDetails { + index: string; + status: IndexStatus; +} + +interface BaseCloudDefendSetupStatus { + indicesDetails: IndexDetails[]; + latestPackageVersion: string; + installedPackagePolicies: number; + healthyAgents: number; + isPluginInitialized: boolean; + // installedPolicyTemplates: PosturePolicyTemplate[]; +} + +interface CloudDefendSetupNotInstalledStatus extends BaseCloudDefendSetupStatus { + status: Extract; +} + +interface CloudDefendSetupInstalledStatus extends BaseCloudDefendSetupStatus { + status: Exclude; + // if installedPackageVersion == undefined but status != 'not-installed' it means the integration was installed in the past and findings were found + // status can be `indexed` but return with undefined package information in this case + installedPackageVersion: string | undefined; +} + +export type CloudDefendSetupStatus = + | CloudDefendSetupInstalledStatus + | CloudDefendSetupNotInstalledStatus; + +export type AgentPolicyStatus = Pick & { agents: number }; + +export interface ControlPolicy { + package_policy: PackagePolicy; + agent_policy: AgentPolicyStatus; +} diff --git a/x-pack/plugins/cloud_defend/common/utils/helpers.test.ts b/x-pack/plugins/cloud_defend/common/utils/helpers.test.ts new file mode 100644 index 0000000000000..24298518aef05 --- /dev/null +++ b/x-pack/plugins/cloud_defend/common/utils/helpers.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; +import { getBenchmarkFromPackagePolicy, getBenchmarkTypeFilter } from './helpers'; + +describe('test helper methods', () => { + it('get default integration type from inputs with multiple enabled types', () => { + const mockPackagePolicy = createPackagePolicyMock(); + + // Both enabled falls back to default + mockPackagePolicy.inputs = [ + { type: 'cloudbeat/cis_k8s', enabled: true, streams: [] }, + { type: 'cloudbeat/cis_eks', enabled: true, streams: [] }, + ]; + const type = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); + expect(type).toMatch('cis_k8s'); + }); + + it('get default integration type from inputs without any enabled types', () => { + const mockPackagePolicy = createPackagePolicyMock(); + + // None enabled falls back to default + mockPackagePolicy.inputs = [ + { type: 'cloudbeat/cis_k8s', enabled: false, streams: [] }, + { type: 'cloudbeat/cis_eks', enabled: false, streams: [] }, + ]; + const type = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); + expect(type).toMatch('cis_k8s'); + }); + + it('get EKS integration type', () => { + const mockPackagePolicy = createPackagePolicyMock(); + + // Single EKS selected + mockPackagePolicy.inputs = [ + { type: 'cloudbeat/cis_eks', enabled: true, streams: [] }, + { type: 'cloudbeat/cis_k8s', enabled: false, streams: [] }, + ]; + const typeEks = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); + expect(typeEks).toMatch('cis_eks'); + }); + + it('get Vanilla K8S integration type', () => { + const mockPackagePolicy = createPackagePolicyMock(); + + // Single k8s selected + mockPackagePolicy.inputs = [ + { type: 'cloudbeat/cis_eks', enabled: false, streams: [] }, + { type: 'cloudbeat/cis_k8s', enabled: true, streams: [] }, + ]; + const typeK8s = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); + expect(typeK8s).toMatch('cis_k8s'); + }); + it('get benchmark type filter based on a benchmark id', () => { + const typeFilter = getBenchmarkTypeFilter('cis_eks'); + expect(typeFilter).toMatch('csp-rule-template.attributes.metadata.benchmark.id: "cis_eks"'); + }); +}); diff --git a/x-pack/plugins/cloud_defend/common/utils/helpers.ts b/x-pack/plugins/cloud_defend/common/utils/helpers.ts new file mode 100644 index 0000000000000..64dc2b7d745fd --- /dev/null +++ b/x-pack/plugins/cloud_defend/common/utils/helpers.ts @@ -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 { Truthy } from 'lodash'; +import { + NewPackagePolicy, + NewPackagePolicyInput, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PackagePolicy, + PackagePolicyInput, +} from '@kbn/fleet-plugin/common'; +import { INTEGRATION_PACKAGE_NAME } from '../constants'; +import type { PolicyId } from '../types'; +import { INPUT_CONTROL } from '../constants'; + +/** + * @example + * declare const foo: Array + * foo.filter(isNonNullable) // foo is Array + */ +export const isNonNullable = (v: T): v is NonNullable => + v !== null && v !== undefined; + +export const truthy = (value: T): value is Truthy => !!value; + +export const extractErrorMessage = (e: unknown, defaultMessage = 'Unknown Error'): string => { + if (e instanceof Error) return e.message; + if (typeof e === 'string') return e; + + return defaultMessage; // TODO: i18n +}; + +export const isEnabledControlInputType = (input: PackagePolicyInput | NewPackagePolicyInput) => + input.enabled; + +export const isCloudDefendPackage = (packageName?: string) => + packageName === INTEGRATION_PACKAGE_NAME; + +export const getControlPolicyFromPackagePolicy = ( + inputs: PackagePolicy['inputs'] | NewPackagePolicy['inputs'] +): PolicyId => { + const enabledInputs = inputs.filter(isEnabledControlInputType); + + // Use the only enabled input + if (enabledInputs.length === 1) { + return getInputType(enabledInputs[0].type); + } + + // Use the default benchmark id for multiple/none selected + return getInputType(INPUT_CONTROL); +}; + +const getInputType = (inputType: string): string => { + // Get the last part of the input type, input type structure: cloudbeat/ + return inputType.split('/')[1]; +}; + +export const CLOUD_DEFEND_FLEET_PACKAGE_KUERY = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${INTEGRATION_PACKAGE_NAME}`; + +export function assert(condition: any, msg?: string): asserts condition { + if (!condition) { + throw new Error(msg); + } +} diff --git a/x-pack/plugins/cloud_defend/common/utils/subscription.test.ts b/x-pack/plugins/cloud_defend/common/utils/subscription.test.ts new file mode 100644 index 0000000000000..e47b887ae520f --- /dev/null +++ b/x-pack/plugins/cloud_defend/common/utils/subscription.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { LicenseType } from '@kbn/licensing-plugin/common/types'; +import { isSubscriptionAllowed } from './subscription'; +import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; + +const ON_PREM_ALLOWED_LICENSES: readonly LicenseType[] = ['enterprise', 'trial']; +const ON_PREM_NOT_ALLOWED_LICENSES: readonly LicenseType[] = ['basic', 'gold', 'platinum']; +const ALL_LICENSE_TYPES: readonly LicenseType[] = [ + 'standard', + ...ON_PREM_NOT_ALLOWED_LICENSES, + ...ON_PREM_NOT_ALLOWED_LICENSES, +]; + +describe('isSubscriptionAllowed', () => { + it('should allow any cloud subscription', () => { + const isCloudEnabled = true; + ALL_LICENSE_TYPES.forEach((licenseType) => { + const license = licenseMock.createLicense({ license: { type: licenseType } }); + expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy(); + }); + }); + + it('should allow enterprise and trial licenses for on-prem', () => { + const isCloudEnabled = false; + ON_PREM_ALLOWED_LICENSES.forEach((licenseType) => { + const license = licenseMock.createLicense({ license: { type: licenseType } }); + expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy(); + }); + }); + + it('should not allow enterprise and trial licenses for on-prem', () => { + const isCloudEnabled = false; + ON_PREM_NOT_ALLOWED_LICENSES.forEach((licenseType) => { + const license = licenseMock.createLicense({ license: { type: licenseType } }); + expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/cloud_defend/common/utils/subscription.ts b/x-pack/plugins/cloud_defend/common/utils/subscription.ts new file mode 100644 index 0000000000000..2d9707681e047 --- /dev/null +++ b/x-pack/plugins/cloud_defend/common/utils/subscription.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; +import { PLUGIN_NAME } from '..'; + +const MINIMUM_NON_CLOUD_LICENSE_TYPE: LicenseType = 'enterprise'; + +export const isSubscriptionAllowed = (isCloudEnabled?: boolean, license?: ILicense): boolean => { + if (isCloudEnabled) { + return true; + } + + if (!license) { + return false; + } + + const licenseCheck = license.check(PLUGIN_NAME, MINIMUM_NON_CLOUD_LICENSE_TYPE); + return licenseCheck.state === 'valid'; +}; diff --git a/x-pack/plugins/cloud_defend/kibana.json b/x-pack/plugins/cloud_defend/kibana.json index 336e20f4129ae..80897f8584ba7 100755 --- a/x-pack/plugins/cloud_defend/kibana.json +++ b/x-pack/plugins/cloud_defend/kibana.json @@ -9,6 +9,6 @@ "description": "Defend for Containers", "server": true, "ui": true, - "requiredPlugins": ["fleet", "kibanaReact"], + "requiredPlugins": ["fleet", "kibanaReact", "usageCollection", "navigation"], "optionalPlugins": [] } diff --git a/x-pack/plugins/cloud_defend/public/application/router.tsx b/x-pack/plugins/cloud_defend/public/application/router.tsx index 9796e20e39995..46ff3e58c3167 100644 --- a/x-pack/plugins/cloud_defend/public/application/router.tsx +++ b/x-pack/plugins/cloud_defend/public/application/router.tsx @@ -11,7 +11,7 @@ import { Redirect, Route, Switch } from 'react-router-dom'; import { cloudDefendPages } from '../common/navigation/constants'; import type { CloudDefendSecuritySolutionContext } from '../types'; import { SecuritySolutionContext } from './security_solution_context'; -import * as pages from '../pages'; +import { Policies } from '../pages/policies'; import { CloudDefendRoute } from './route'; const queryClient = new QueryClient({ @@ -27,7 +27,7 @@ export const CloudDefendRouter = ({ securitySolutionContext }: CloudDefendRouter const routerElement = ( - + diff --git a/x-pack/plugins/cloud_defend/public/common/api/use_cloud_defend_integration.tsx b/x-pack/plugins/cloud_defend/public/common/api/use_cloud_defend_integration.tsx new file mode 100644 index 0000000000000..c41ffdab0851c --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/api/use_cloud_defend_integration.tsx @@ -0,0 +1,26 @@ +/* + * 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 } from '@tanstack/react-query'; +import { + epmRouteService, + type GetInfoResponse, + type DefaultPackagesInstallationError, +} from '@kbn/fleet-plugin/common'; +import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; +import { useKibana } from '../hooks/use_kibana'; + +/** + * This hook will find our integration and return its PackageInfo + * */ +export const useCloudDefendIntegration = () => { + const { http } = useKibana().services; + + return useQuery(['integrations'], () => + http.get(epmRouteService.getInfoPath(INTEGRATION_PACKAGE_NAME)) + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts new file mode 100644 index 0000000000000..7c5d4eb8dc31b --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; +import { useKibana } from '../hooks/use_kibana'; +import { CspSetupStatus } from '../../../common/types'; +import { STATUS_ROUTE_PATH } from '../../../common/constants'; + +const getCspSetupStatusQueryKey = 'csp_status_key'; + +export const useCspSetupStatusApi = ({ + options, +}: { options?: UseQueryOptions } = {}) => { + const { http } = useKibana().services; + return useQuery( + [getCspSetupStatusQueryKey], + () => http.get(STATUS_ROUTE_PATH), + options + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/common/constants.ts b/x-pack/plugins/cloud_defend/public/common/constants.ts index d0baec8804ff7..1e101a47c0122 100644 --- a/x-pack/plugins/cloud_defend/public/common/constants.ts +++ b/x-pack/plugins/cloud_defend/public/common/constants.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +export const DEFAULT_VISIBLE_ROWS_PER_PAGE = 10; // generic default # of table rows to show (currently we only have a list of policies) +export const LOCAL_STORAGE_PAGE_SIZE = 'cloudDefend:userPageSize'; export const VALID_SELECTOR_NAME_REGEX = /^[a-z0-9][a-z0-9_\-]+$/i; // alphanumberic (no - or _ allowed on first char) export const MAX_SELECTOR_NAME_LENGTH = 128; // chars export const MAX_CONDITION_VALUE_LENGTH_BYTES = 511; diff --git a/x-pack/plugins/cloud_defend/public/common/hooks/use_kibana.ts b/x-pack/plugins/cloud_defend/public/common/hooks/use_kibana.ts new file mode 100644 index 0000000000000..261afc07db00f --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/hooks/use_kibana.ts @@ -0,0 +1,14 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { useKibana as useKibanaBase } from '@kbn/kibana-react-plugin/public'; +import type { CloudDefendPluginStartDeps } from '../../types'; + +type CloudDefendKibanaContext = CoreStart & CloudDefendPluginStartDeps; + +export const useKibana = () => useKibanaBase(); diff --git a/x-pack/plugins/cloud_defend/public/common/hooks/use_page_size.ts b/x-pack/plugins/cloud_defend/public/common/hooks/use_page_size.ts new file mode 100644 index 0000000000000..314dfbe661d93 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/hooks/use_page_size.ts @@ -0,0 +1,26 @@ +/* + * 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 useLocalStorage from 'react-use/lib/useLocalStorage'; +import { DEFAULT_VISIBLE_ROWS_PER_PAGE } from '../constants'; + +/** + * @description handles persisting the users table row size selection + */ +export const usePageSize = (localStorageKey: string) => { + const [persistedPageSize, setPersistedPageSize] = useLocalStorage( + localStorageKey, + DEFAULT_VISIBLE_ROWS_PER_PAGE + ); + + let pageSize: number = DEFAULT_VISIBLE_ROWS_PER_PAGE; + + if (persistedPageSize) { + pageSize = persistedPageSize; + } + + return { pageSize, setPageSize: setPersistedPageSize }; +}; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts index 9942c95f08094..863630770750f 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { cloudPosturePages } from './constants'; -import type { CloudSecurityPosturePageId, CspPage } from './types'; +import { cloudDefendPages } from './constants'; +import type { CloudDefendPageId, CloudDefendPage } from './types'; -interface CloudSecurityPostureLinkItem { +interface CloudDefendLinkItem { id: TId; title: string; path: string; } -interface CloudSecurityPostureNavTab { +interface CloudDefendNavTab { id: TId; name: string; href: string; @@ -23,27 +23,27 @@ interface CloudSecurityPostureNavTab( - cloudSecurityPosturePage: CspPage -): CloudSecurityPostureLinkItem => ({ - id: cloudPosturePages[cloudSecurityPosturePage].id as TId, - title: cloudPosturePages[cloudSecurityPosturePage].name, - path: cloudPosturePages[cloudSecurityPosturePage].path, +export const getSecuritySolutionLink = ( + cloudDefendPage: CloudDefendPage +): CloudDefendLinkItem => ({ + id: cloudDefendPages[cloudDefendPage].id as TId, + title: cloudDefendPages[cloudDefendPage].name, + path: cloudDefendPages[cloudDefendPage].path, }); /** - * Gets the cloud security posture link properties of a CSP page for navigation in the old security solution navigation. - * @param cloudSecurityPosturePage the name of the cloud posture page. + * Gets the link properties of a Cloud Defend page for navigation in the old security solution navigation. + * @param cloudDefendPage the name of the cloud defend page. * @param basePath the base path for links. */ -export const getSecuritySolutionNavTab = ( - cloudSecurityPosturePage: CspPage, +export const getSecuritySolutionNavTab = ( + cloudDefendPage: CloudDefendPage, basePath: string -): CloudSecurityPostureNavTab => ({ - id: cloudPosturePages[cloudSecurityPosturePage].id as TId, - name: cloudPosturePages[cloudSecurityPosturePage].name, - href: `${basePath}${cloudPosturePages[cloudSecurityPosturePage].path}`, - disabled: !!cloudPosturePages[cloudSecurityPosturePage].disabled, +): CloudDefendNavTab => ({ + id: cloudDefendPages[cloudDefendPage].id as TId, + name: cloudDefendPages[cloudDefendPage].name, + href: `${basePath}${cloudDefendPages[cloudDefendPage].path}`, + disabled: !!cloudDefendPages[cloudDefendPage].disabled, }); diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/types.ts b/x-pack/plugins/cloud_defend/public/common/navigation/types.ts index 13530cae13985..56da6ac4c3948 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/types.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/types.ts @@ -20,4 +20,4 @@ export type CloudDefendPage = 'policies'; * All the IDs for the cloud defend pages. * This needs to match the cloud defend page entries in `SecurityPageName` in `x-pack/plugins/security_solution/common/constants.ts`. */ -export type CloudDefendPageId = 'cloud_security_posture-policies'; +export type CloudDefendPageId = 'cloud_defend-policies'; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/use_csp_integration_link.ts b/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts similarity index 55% rename from x-pack/plugins/cloud_defend/public/common/navigation/use_csp_integration_link.ts rename to x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts index 8d6e0f6c38583..6355346647d7e 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/use_csp_integration_link.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts @@ -6,24 +6,22 @@ */ import { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public'; -import type { PosturePolicyTemplate } from '../../../common/types'; -import { useCisKubernetesIntegration } from '../api/use_cis_kubernetes_integration'; +import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; +import { useCloudDefendIntegration } from '../api/use_cloud_defend_integration'; import { useKibana } from '../hooks/use_kibana'; -export const useCspIntegrationLink = ( - policyTemplate: PosturePolicyTemplate -): string | undefined => { +export const useCloudDefendIntegrationLink = (): string | undefined => { const { http } = useKibana().services; - const cisIntegration = useCisKubernetesIntegration(); + const cloudDefendIntegration = useCloudDefendIntegration(); - if (!cisIntegration.isSuccess) return; + if (!cloudDefendIntegration.isSuccess) return; const path = pagePathGetters .add_integration_to_policy({ - integration: policyTemplate, + integration: INTEGRATION_PACKAGE_NAME, pkgkey: pkgKeyFromPackageInfo({ - name: cisIntegration.data.item.name, - version: cisIntegration.data.item.version, + name: cloudDefendIntegration.data.item.name, + version: cloudDefendIntegration.data.item.version, }), }) .join(''); diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.ts b/x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.ts deleted file mode 100644 index 350d7a8f38dac..0000000000000 --- a/x-pack/plugins/cloud_defend/public/common/navigation/use_navigate_to_cis_integration_policies.ts +++ /dev/null @@ -1,35 +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 { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public'; -import { useCisKubernetesIntegration } from '../api/use_cis_kubernetes_integration'; -import { useKibana } from '../hooks/use_kibana'; - -export const useCISIntegrationPoliciesLink = ({ - addAgentToPolicyId = '', - integration = '', -}: { - addAgentToPolicyId?: string; - integration?: string; -}): string | undefined => { - const { http } = useKibana().services; - const cisIntegration = useCisKubernetesIntegration(); - if (!cisIntegration.isSuccess) return; - - const path = pagePathGetters - .integration_details_policies({ - addAgentToPolicyId, - integration, - pkgkey: pkgKeyFromPackageInfo({ - name: cisIntegration.data.item.name, - version: cisIntegration.data.item.version, - }), - }) - .join(''); - - return http.basePath.prepend(path); -}; diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx new file mode 100644 index 0000000000000..749aa1ccb038a --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx @@ -0,0 +1,357 @@ +/* + * 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 { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; +import Chance from 'chance'; +import { + DEFAULT_NO_DATA_TEST_SUBJECT, + ERROR_STATE_TEST_SUBJECT, + isCommonError, + LOADING_STATE_TEST_SUBJECT, + PACKAGE_NOT_INSTALLED_TEST_SUBJECT, + SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT, +} from './cloud_posture_page'; +import { createReactQueryResponse } from '../test/fixtures/react_query'; +import { TestProvider } from '../test/test_provider'; +import { coreMock } from '@kbn/core/public/mocks'; +import { render, screen } from '@testing-library/react'; +import React, { ComponentProps } from 'react'; +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 { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link'; + +const chance = new Chance(); + +jest.mock('../common/api/use_setup_status_api'); +jest.mock('../common/hooks/use_subscription_status'); +jest.mock('../common/navigation/use_csp_integration_link'); + +describe('', () => { + beforeEach(() => { + jest.resetAllMocks(); + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'indexed' }, + }) + ); + + (useSubscriptionStatus as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: true, + }) + ); + }); + + const renderCloudPosturePage = ( + props: ComponentProps = { children: null } + ) => { + const mockCore = coreMock.createStart(); + + render( + + + + ); + }; + + it('renders children if setup status is indexed', () => { + const children = chance.sentence(); + renderCloudPosturePage({ children }); + + expect(screen.getByText(children)).toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders default loading state when the subscription query is loading', () => { + (useSubscriptionStatus as jest.Mock).mockImplementation( + () => + createReactQueryResponse({ + status: 'loading', + }) as unknown as UseQueryResult + ); + + const children = chance.sentence(); + renderCloudPosturePage({ children }); + + expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders default error state when the subscription query has an error', () => { + (useSubscriptionStatus as jest.Mock).mockImplementation( + () => + createReactQueryResponse({ + status: 'error', + error: new Error('error'), + }) as unknown as UseQueryResult + ); + + const children = chance.sentence(); + renderCloudPosturePage({ children }); + + expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders subscription not allowed prompt if subscription is not installed', () => { + (useSubscriptionStatus as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: false, + }) + ); + + const children = chance.sentence(); + renderCloudPosturePage({ children }); + + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders integrations installation prompt if integration is not installed', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + createReactQueryResponse({ + status: 'success', + data: { status: 'not-installed' }, + }) + ); + (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + + const children = chance.sentence(); + renderCloudPosturePage({ children }); + + expect(screen.getByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders default loading state when the integration query is loading', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation( + () => + createReactQueryResponse({ + status: 'loading', + }) as unknown as UseQueryResult + ); + + const children = chance.sentence(); + renderCloudPosturePage({ children }); + + expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders default error state when the integration query has an error', () => { + (useCspSetupStatusApi as jest.Mock).mockImplementation( + () => + createReactQueryResponse({ + status: 'error', + error: new Error('error'), + }) as unknown as UseQueryResult + ); + + const children = chance.sentence(); + renderCloudPosturePage({ children }); + + expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders default loading text when query isLoading', () => { + const query = createReactQueryResponse({ + status: 'loading', + }) as unknown as UseQueryResult; + + const children = chance.sentence(); + renderCloudPosturePage({ children, query }); + + expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders default loading text when query is idle', () => { + const query = createReactQueryResponse({ + status: 'idle', + }) as unknown as UseQueryResult; + + const children = chance.sentence(); + renderCloudPosturePage({ children, query }); + + expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders default error texts when query isError', () => { + const error = chance.sentence(); + const message = chance.sentence(); + const statusCode = chance.integer(); + + const query = createReactQueryResponse({ + status: 'error', + error: { + body: { + error, + message, + statusCode, + }, + }, + }) as unknown as UseQueryResult; + + const children = chance.sentence(); + renderCloudPosturePage({ children, query }); + + [error, message, statusCode].forEach((text) => + expect(screen.getByText(text, { exact: false })).toBeInTheDocument() + ); + expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('prefers custom error render', () => { + const error = chance.sentence(); + const message = chance.sentence(); + const statusCode = chance.integer(); + + const query = createReactQueryResponse({ + status: 'error', + error: { + body: { + error, + message, + statusCode, + }, + }, + }) as unknown as UseQueryResult; + + const children = chance.sentence(); + renderCloudPosturePage({ + children, + query, + errorRender: (err) =>
{isCommonError(err) && err.body.message}
, + }); + + expect(screen.getByText(message)).toBeInTheDocument(); + [error, statusCode].forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument()); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('prefers custom loading render', () => { + const loading = chance.sentence(); + + const query = createReactQueryResponse({ + status: 'loading', + }) as unknown as UseQueryResult; + + const children = chance.sentence(); + renderCloudPosturePage({ + children, + query, + loadingRender: () =>
{loading}
, + }); + + expect(screen.getByText(loading)).toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('renders no data prompt when query data is undefined', () => { + const query = createReactQueryResponse({ + status: 'success', + data: undefined, + }) as unknown as UseQueryResult; + + const children = chance.sentence(); + renderCloudPosturePage({ children, query }); + + expect(screen.getByTestId(DEFAULT_NO_DATA_TEST_SUBJECT)).toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); + + it('prefers custom no data prompt', () => { + const pageTitle = chance.sentence(); + const solution = chance.sentence(); + const docsLink = chance.sentence(); + const noDataRenderer = () => ( + + ); + + const query = createReactQueryResponse({ + status: 'success', + data: undefined, + }) as unknown as UseQueryResult; + + const children = chance.sentence(); + renderCloudPosturePage({ + children, + query, + noDataRenderer, + }); + + expect(screen.getByText(pageTitle)).toBeInTheDocument(); + expect(screen.getAllByText(solution, { exact: false })[0]).toBeInTheDocument(); + expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByText(children)).not.toBeInTheDocument(); + expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); + expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx new file mode 100644 index 0000000000000..50fbcb8661e9c --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx @@ -0,0 +1,300 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { + EuiButton, + EuiEmptyPrompt, + EuiImage, + EuiFlexGroup, + EuiFlexItem, + EuiLink, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { NoDataPage, NoDataPageProps } from '@kbn/kibana-react-plugin/public'; +import { css } from '@emotion/react'; +// import { SubscriptionNotAllowed } from './subscription_not_allowed'; +// import { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; +import { FullSizeCenteredPage } from '../full_size_page'; +// import { useCloudDefendSetupStatusApi } from '../common/api/use_setup_status_api'; +import { CloudDefendLoadingState } from '../loading_state'; +// import { useCloudDefendIntegrationLink } from '../common/navigation/use_cloud_defend_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'; + +interface CommonError { + body: { + error: string; + message: string; + statusCode: number; + }; +} + +export const isCommonError = (error: unknown): error is CommonError => { + if ( + !(error as any)?.body || + !(error as any)?.body?.error || + !(error as any)?.body?.message || + !(error as any)?.body?.statusCode + ) { + return false; + } + + return true; +}; + +export interface CloudDefendNoDataPageProps { + pageTitle: NoDataPageProps['pageTitle']; + docsLink: NoDataPageProps['docsLink']; + actionHref: NoDataPageProps['actions']['elasticAgent']['href']; + actionTitle: NoDataPageProps['actions']['elasticAgent']['title']; + actionDescription: NoDataPageProps['actions']['elasticAgent']['description']; + testId: string; +} + +export const CloudDefendNoDataPage = ({ + pageTitle, + docsLink, + actionHref, + actionTitle, + actionDescription, + testId, +}: CloudDefendNoDataPageProps) => { + return ( + :nth-child(3) { + display: block; + margin: auto; + width: 450px; + } + `} + pageTitle={pageTitle} + solution={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel', { + defaultMessage: 'Cloud Security Posture', + })} + docsLink={docsLink} + logo="logoSecurity" + actions={{ + elasticAgent: { + href: actionHref, + isDisabled: !actionHref, + title: actionTitle, + description: actionDescription, + }, + }} + /> + ); +}; + +const packageNotInstalledRenderer = ({ + kspmIntegrationLink, + cspmIntegrationLink, +}: { + kspmIntegrationLink?: string; + cspmIntegrationLink?: string; +}) => { + return ( + + } + title={ +

+ +

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

+ + + + ), + }} + /> +

+ } + actions={ + + + + + + + + + + + + + } + /> +
+ ); +}; + +const defaultLoadingRenderer = () => ( + + + +); + +const defaultErrorRenderer = (error: unknown) => ( + + + + + } + body={ + isCommonError(error) ? ( +

+ +

+ ) : undefined + } + /> +
+); + +const defaultNoDataRenderer = () => ( + + + +); + +/* const subscriptionNotAllowedRenderer = () => ( + + + +);*/ + +interface CloudPosturePageProps { + children: React.ReactNode; + query?: UseQueryResult; + loadingRender?: () => React.ReactNode; + errorRender?: (error: TError) => React.ReactNode; + noDataRenderer?: () => React.ReactNode; +} + +export const CloudDefendPage = ({ + children, + query, + loadingRender = defaultLoadingRenderer, + errorRender = defaultErrorRenderer, + noDataRenderer = defaultNoDataRenderer, +}: CloudPosturePageProps) => { + const subscriptionStatus = useSubscriptionStatus(); + const getSetupStatus = useCloudDefendSetupStatusApi(); + const integrationLink = useCloudDefendIntegrationLink(CONTROL_POLICY_TEMPLATE); + + const render = () => { + /* if (subscriptionStatus.isError) { + return defaultErrorRenderer(subscriptionStatus.error); + } + + if (subscriptionStatus.isLoading) { + return defaultLoadingRenderer(); + } + + if (!subscriptionStatus.data) { + return subscriptionNotAllowedRenderer(); + }*/ + + if (getSetupStatus.isError) { + return defaultErrorRenderer(getSetupStatus.error); + } + + if (getSetupStatus.isLoading) { + return defaultLoadingRenderer(); + } + + if (getSetupStatus.data.status === 'not-installed') { + return packageNotInstalledRenderer({ kspmIntegrationLink, cspmIntegrationLink }); + } + + if (!query) { + return children; + } + + if (query.isError) { + return errorRender(query.error); + } + + if (query.isLoading) { + return loadingRender(); + } + + if (!query.data) { + return noDataRenderer(); + } + + return children; + }; + + return <>{render()}; +}; diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page_title/index.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page_title/index.tsx new file mode 100644 index 0000000000000..43422ff2db1e6 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page_title/index.tsx @@ -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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; + +export const CloudDefendPageTitle = ({ title }: { title: string }) => ( + + + +

{title}

+
+
+
+); diff --git a/x-pack/plugins/cloud_defend/public/components/full_size_page/index.tsx b/x-pack/plugins/cloud_defend/public/components/full_size_page/index.tsx new file mode 100644 index 0000000000000..4e68797c8c21f --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/full_size_page/index.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 { EuiFlexGroup, type CommonProps } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React from 'react'; + +// Keep this component lean as it is part of the main app bundle +export const FullSizeCenteredPage = ({ + children, + ...rest +}: { children: React.ReactNode } & CommonProps) => ( + + {children} + +); diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.test.tsx b/x-pack/plugins/cloud_defend/public/components/policies_table/index.test.tsx similarity index 100% rename from x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.test.tsx rename to x-pack/plugins/cloud_defend/public/components/policies_table/index.test.tsx diff --git a/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx b/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx new file mode 100644 index 0000000000000..39a5421ba653f --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx @@ -0,0 +1,172 @@ +/* + * 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 { + EuiBasicTable, + type EuiBasicTableColumn, + type EuiBasicTableProps, + type Pagination, + type CriteriaWithPagination, + EuiLink, +} from '@elastic/eui'; +import React from 'react'; +import { generatePath } from 'react-router-dom'; +import { pagePathGetters } from '@kbn/fleet-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { TimestampTableCell } from '../timestamp_table_cell'; +import type { ControlPolicy } from '../../../common/types'; +import { useKibana } from '../../common/hooks/use_kibana'; +import * as TEST_SUBJ from '../../pages/policies/test_subjects'; + +interface PoliciesTableProps + extends Pick< + EuiBasicTableProps, + 'loading' | 'error' | 'noItemsMessage' | 'sorting' + >, + Pagination { + policies: ControlPolicy[]; + setQuery(pagination: CriteriaWithPagination): void; + 'data-test-subj'?: string; +} + +const AgentPolicyButtonLink = ({ name, id: policyId }: { name: string; id: string }) => { + const { http } = useKibana().services; + const [fleetBase, path] = pagePathGetters.policy_details({ policyId }); + + return {name}; +}; + +const IntegrationButtonLink = ({ + packageName, + policyId, + packagePolicyId, +}: { + packageName: string; + packagePolicyId: string; + policyId: string; +}) => { + const { application } = useKibana().services; + + return ( + + {packageName} + + ); +}; + +const POLICIES_TABLE_COLUMNS: Array> = [ + { + field: 'package_policy.name', + name: i18n.translate('xpack.cloudDefend.policies.policiesTable.integrationNameColumnTitle', { + defaultMessage: 'Integration Name', + }), + render: (packageName, policy) => ( + + ), + truncateText: true, + sortable: true, + 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.INTEGRATION_NAME, + }, + { + field: 'rules_count', + name: i18n.translate('xpack.cloudDefend.policies.policiesTable.rulesColumnTitle', { + defaultMessage: 'Rules', + }), + truncateText: true, + 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.RULES, + }, + { + field: 'agent_policy.name', + name: i18n.translate('xpack.cloudDefend.policies.policiesTable.agentPolicyColumnTitle', { + defaultMessage: 'Agent Policy', + }), + render: (name, policy) => , + truncateText: true, + 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.AGENT_POLICY, + }, + { + field: 'agent_policy.agents', + name: i18n.translate('xpack.cloudDefend.policies.policiesTable.numberOfAgentsColumnTitle', { + defaultMessage: 'Number of Agents', + }), + truncateText: true, + 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.NUMBER_OF_AGENTS, + }, + { + field: 'package_policy.created_by', + name: i18n.translate('xpack.cloudDefend.policies.policiesTable.createdByColumnTitle', { + defaultMessage: 'Created by', + }), + dataType: 'string', + truncateText: true, + sortable: true, + 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.CREATED_BY, + }, + { + field: 'package_policy.created_at', + name: i18n.translate('xpack.cloudDefend.policies.policiesTable.createdAtColumnTitle', { + defaultMessage: 'Created at', + }), + dataType: 'date', + truncateText: true, + render: (timestamp: ControlPolicy['package_policy']['created_at']) => ( + + ), + sortable: true, + 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.CREATED_AT, + }, +]; + +export const PoliciesTable = ({ + policies, + pageIndex, + pageSize, + totalItemCount, + loading, + error, + setQuery, + noItemsMessage, + sorting, + ...rest +}: PoliciesTableProps) => { + const pagination: Pagination = { + pageIndex: Math.max(pageIndex - 1, 0), + pageSize, + totalItemCount, + }; + + const onChange = ({ page, sort }: CriteriaWithPagination) => { + setQuery({ page: { ...page, index: page.index + 1 }, sort }); + }; + + return ( + [item.agent_policy.id, item.package_policy.id].join('/')} + pagination={pagination} + onChange={onChange} + tableLayout="fixed" + loading={loading} + noItemsMessage={noItemsMessage} + error={error} + sorting={sorting} + /> + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/components/timestamp_table_cell/index.tsx b/x-pack/plugins/cloud_defend/public/components/timestamp_table_cell/index.tsx new file mode 100644 index 0000000000000..b6b6934b92cde --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/timestamp_table_cell/index.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import moment, { type MomentInput } from 'moment'; +import { EuiToolTip, formatDate } from '@elastic/eui'; +import { useUiSetting } from '@kbn/kibana-react-plugin/public'; + +const DEFAULT_DATE_FORMAT = 'dateFormat'; + +export const TimestampTableCell = ({ timestamp }: { timestamp: MomentInput }) => { + const dateFormat = useUiSetting(DEFAULT_DATE_FORMAT); + const formatted = formatDate(timestamp, dateFormat); + + return ( + + {moment(timestamp).fromNow()} + + ); +}; diff --git a/x-pack/plugins/cloud_defend/public/index.ts b/x-pack/plugins/cloud_defend/public/index.ts index fd8099aa2ed11..9dcf8bd5b2760 100755 --- a/x-pack/plugins/cloud_defend/public/index.ts +++ b/x-pack/plugins/cloud_defend/public/index.ts @@ -6,6 +6,11 @@ */ import { CloudDefendPlugin } from './plugin'; +export type { CloudDefendSecuritySolutionContext } from './types'; +export { getSecuritySolutionLink } from './common/navigation/security_solution_links'; +export { CLOUD_DEFEND_BASE_PATH } from './common/navigation/constants'; +export type { CloudDefendPageId } from './common/navigation/types'; + // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. export function plugin() { diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx deleted file mode 100644 index ed276dc374744..0000000000000 --- a/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks_table.tsx +++ /dev/null @@ -1,200 +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 { - EuiBasicTable, - type EuiBasicTableColumn, - type EuiBasicTableProps, - type Pagination, - type CriteriaWithPagination, - EuiLink, -} from '@elastic/eui'; -import React from 'react'; -import { generatePath } from 'react-router-dom'; -import { pagePathGetters } from '@kbn/fleet-plugin/public'; -import { i18n } from '@kbn/i18n'; -import type { PackagePolicy } from '@kbn/fleet-plugin/common'; -import { TimestampTableCell } from '../../components/timestamp_table_cell'; -import type { Benchmark } from '../../../common/types'; -import { useKibana } from '../../common/hooks/use_kibana'; -import { benchmarksNavigation } from '../../common/navigation/constants'; -import * as TEST_SUBJ from './test_subjects'; -import { getEnabledCspIntegrationDetails } from '../../common/utils/get_enabled_csp_integration_details'; - -interface BenchmarksTableProps - extends Pick, 'loading' | 'error' | 'noItemsMessage' | 'sorting'>, - Pagination { - benchmarks: Benchmark[]; - setQuery(pagination: CriteriaWithPagination): void; - 'data-test-subj'?: string; -} - -const AgentPolicyButtonLink = ({ name, id: policyId }: { name: string; id: string }) => { - const { http } = useKibana().services; - const [fleetBase, path] = pagePathGetters.policy_details({ policyId }); - - return {name}; -}; - -const IntegrationButtonLink = ({ - packageName, - policyId, - packagePolicyId, -}: { - packageName: string; - packagePolicyId: string; - policyId: string; -}) => { - const { application } = useKibana().services; - - return ( - - {packageName} - - ); -}; - -const BENCHMARKS_TABLE_COLUMNS: Array> = [ - { - field: 'package_policy.name', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.integrationNameColumnTitle', { - defaultMessage: 'Integration Name', - }), - render: (packageName, benchmark) => ( - - ), - truncateText: true, - sortable: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.INTEGRATION_NAME, - }, - { - field: 'rules_count', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.rulesColumnTitle', { - defaultMessage: 'Rules', - }), - truncateText: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.RULES, - }, - { - field: 'package_policy', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.integrationColumnTitle', { - defaultMessage: 'Integration', - }), - dataType: 'string', - truncateText: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.INTEGRATION, - render: (field: PackagePolicy) => { - const enabledIntegration = getEnabledCspIntegrationDetails(field); - return enabledIntegration?.integration?.shortName || ' '; - }, - }, - { - field: 'package_policy', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.monitoringColumnTitle', { - defaultMessage: 'Monitoring', - }), - dataType: 'string', - truncateText: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.MONITORING, - render: (field: PackagePolicy) => { - const enabledIntegration = getEnabledCspIntegrationDetails(field); - return enabledIntegration?.enabledIntegrationOption?.name || ' '; - }, - }, - { - field: 'agent_policy.name', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.agentPolicyColumnTitle', { - defaultMessage: 'Agent Policy', - }), - render: (name, benchmark) => ( - - ), - truncateText: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.AGENT_POLICY, - }, - { - field: 'agent_policy.agents', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.numberOfAgentsColumnTitle', { - defaultMessage: 'Number of Agents', - }), - truncateText: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.NUMBER_OF_AGENTS, - }, - { - field: 'package_policy.created_by', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.createdByColumnTitle', { - defaultMessage: 'Created by', - }), - dataType: 'string', - truncateText: true, - sortable: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.CREATED_BY, - }, - { - field: 'package_policy.created_at', - name: i18n.translate('xpack.csp.benchmarks.benchmarksTable.createdAtColumnTitle', { - defaultMessage: 'Created at', - }), - dataType: 'date', - truncateText: true, - render: (timestamp: Benchmark['package_policy']['created_at']) => ( - - ), - sortable: true, - 'data-test-subj': TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS.CREATED_AT, - }, -]; - -export const BenchmarksTable = ({ - benchmarks, - pageIndex, - pageSize, - totalItemCount, - loading, - error, - setQuery, - noItemsMessage, - sorting, - ...rest -}: BenchmarksTableProps) => { - const pagination: Pagination = { - pageIndex: Math.max(pageIndex - 1, 0), - pageSize, - totalItemCount, - }; - - const onChange = ({ page, sort }: CriteriaWithPagination) => { - setQuery({ page: { ...page, index: page.index + 1 }, sort }); - }; - - return ( - [item.agent_policy.id, item.package_policy.id].join('/')} - pagination={pagination} - onChange={onChange} - tableLayout="fixed" - loading={loading} - noItemsMessage={noItemsMessage} - error={error} - sorting={sorting} - /> - ); -}; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/benchmarks.test.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx similarity index 100% rename from x-pack/plugins/cloud_defend/public/pages/policies/benchmarks.test.tsx rename to x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.ts b/x-pack/plugins/cloud_defend/public/pages/policies/index.ts deleted file mode 100644 index ec35f87e70811..0000000000000 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { Policies } from './policies'; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/policies.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx similarity index 77% rename from x-pack/plugins/cloud_defend/public/pages/policies/policies.tsx rename to x-pack/plugins/cloud_defend/public/pages/policies/index.tsx index dc5b35a17241a..a8ff9c7c01f98 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/policies.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx @@ -22,22 +22,19 @@ import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; -import { CloudPosturePageTitle } from '../../components/cloud_posture_page_title'; -import { CloudPosturePage } from '../../components/cloud_posture_page'; -import { BenchmarksTable } from './benchmarks_table'; -import { - useCspBenchmarkIntegrations, - UseCspBenchmarkIntegrationsProps, -} from './use_csp_benchmark_integrations'; +import { CloudDefendPageTitle } from '../../components/cloud_defend_page_title'; +import { CloudDefendPage } from '../../components/cloud_defend_page'; +import { PoliciesTable } from '../../components/policies_table'; +import { useCloudDefendPolicies, UseCloudDefendPoliciesProps } from './use_cloud_defend_policies'; import { extractErrorMessage } from '../../../common/utils/helpers'; import * as TEST_SUBJ from './test_subjects'; -import { LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY } from '../../common/constants'; +import { LOCAL_STORAGE_PAGE_SIZE } from '../../common/constants'; import { usePageSize } from '../../common/hooks/use_page_size'; import { useKibana } from '../../common/hooks/use_kibana'; const SEARCH_DEBOUNCE_MS = 300; -const AddCisIntegrationButton = () => { +const AddIntegrationButton = () => { const { http } = useKibana().services; const integrationsPath = pagePathGetters @@ -61,7 +58,7 @@ const AddCisIntegrationButton = () => { ); }; -const BenchmarkEmptyState = ({ name }: { name: string }) => ( +const EmptyState = ({ name }: { name: string }) => (
{ @@ -109,7 +106,7 @@ const TotalIntegrationsCount = ({ ); -const BenchmarkSearchField = ({ +const SearchField = ({ onSearch, isLoading, }: Required>) => { @@ -135,9 +132,9 @@ const BenchmarkSearchField = ({ ); }; -export const Benchmarks = () => { - const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_BENCHMARK_KEY); - const [query, setQuery] = useState({ +export const Policies = () => { + const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE); + const [query, setQuery] = useState({ name: '', page: 1, perPage: pageSize, @@ -145,26 +142,25 @@ export const Benchmarks = () => { sortOrder: 'asc', }); - const queryResult = useCspBenchmarkIntegrations(query); + const queryResult = useCloudDefendPolicies(query); const totalItemCount = queryResult.data?.total || 0; return ( - + } - rightSideItems={[]} + rightSideItems={[]} bottomBorder /> - setQuery((current) => ({ ...current, name }))} /> @@ -174,15 +170,14 @@ export const Benchmarks = () => { totalCount={totalItemCount} /> - { page: page.index, perPage: page.size, sortField: - (sort?.field as UseCspBenchmarkIntegrationsProps['sortField']) || current.sortField, + (sort?.field as UseCloudDefendPoliciesProps['sortField']) || current.sortField, sortOrder: sort?.direction || current.sortOrder, })); }} noItemsMessage={ queryResult.isSuccess && !queryResult.data.total ? ( - + ) : undefined } /> - + ); }; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts b/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts index 3e75715abf32f..91df8ace83b2c 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts +++ b/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts @@ -5,16 +5,16 @@ * 2.0. */ -export const BENCHMARKS_PAGE_HEADER = 'benchmarks-page-header'; -export const BENCHMARKS_TABLE_DATA_TEST_SUBJ = 'csp_benchmarks_table'; -export const ADD_INTEGRATION_TEST_SUBJ = 'csp_add_integration'; -export const BENCHMARKS_TABLE_COLUMNS = { - INTEGRATION_NAME: 'benchmarks-table-column-integration-name', - MONITORING: 'benchmarks-table-column-monitoring', - RULES: 'benchmarks-table-column-rules', - INTEGRATION: 'benchmarks-table-column-integration', - AGENT_POLICY: 'benchmarks-table-column-agent-policy', - NUMBER_OF_AGENTS: 'benchmarks-table-column-number-of-agents', - CREATED_BY: 'benchmarks-table-column-created-by', - CREATED_AT: 'benchmarks-table-column-created-at', +export const POLICIES_PAGE_HEADER = 'policies-page-header'; +export const POLICIES_TABLE_DATA_TEST_SUBJ = 'cloud_defend_policies_table'; +export const ADD_INTEGRATION_TEST_SUBJ = 'cloud_defend_add_integration'; +export const POLICIES_TABLE_COLUMNS = { + INTEGRATION_NAME: 'policies-table-column-integration-name', + MONITORING: 'policies-table-column-monitoring', + RULES: 'policies-table-column-rules', + INTEGRATION: 'policies-table-column-integration', + AGENT_POLICY: 'policies-table-column-agent-policy', + NUMBER_OF_AGENTS: 'policies-table-column-number-of-agents', + CREATED_BY: 'policies-table-column-created-by', + CREATED_AT: 'policies-table-column-created-at', }; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/use_csp_benchmark_integrations.ts b/x-pack/plugins/cloud_defend/public/pages/policies/use_cloud_defend_policies.ts similarity index 54% rename from x-pack/plugins/cloud_defend/public/pages/policies/use_csp_benchmark_integrations.ts rename to x-pack/plugins/cloud_defend/public/pages/policies/use_cloud_defend_policies.ts index ccdc3650b2596..a327e9b878cc8 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/use_csp_benchmark_integrations.ts +++ b/x-pack/plugins/cloud_defend/public/pages/policies/use_cloud_defend_policies.ts @@ -7,31 +7,31 @@ import { useQuery } from '@tanstack/react-query'; import type { ListResult } from '@kbn/fleet-plugin/common'; -import { BENCHMARKS_ROUTE_PATH } from '../../../common/constants'; -import type { BenchmarksQueryParams } from '../../../common/schemas/benchmark'; +import { POLICIES_ROUTE_PATH } from '../../../common/constants'; +import type { PoliciesQueryParams } from '../../../common/schemas/policies'; import { useKibana } from '../../common/hooks/use_kibana'; -import type { Benchmark } from '../../../common/types'; +import type { Policy } from '../../../common/types'; -const QUERY_KEY = 'csp_benchmark_integrations'; +const QUERY_KEY = 'cloud_defend_policies'; -export interface UseCspBenchmarkIntegrationsProps { +export interface UseCloudDefendPoliciesProps { name: string; page: number; perPage: number; - sortField: BenchmarksQueryParams['sort_field']; - sortOrder: BenchmarksQueryParams['sort_order']; + sortField: PoliciesQueryParams['sort_field']; + sortOrder: PoliciesQueryParams['sort_order']; } -export const useCspBenchmarkIntegrations = ({ +export const useCloudDefendPolicies = ({ name, perPage, page, sortField, sortOrder, -}: UseCspBenchmarkIntegrationsProps) => { +}: UseCloudDefendPoliciesProps) => { const { http } = useKibana().services; - const query: BenchmarksQueryParams = { - benchmark_name: name, + const query: PoliciesQueryParams = { + policy_name: name, per_page: perPage, page, sort_field: sortField, @@ -40,7 +40,7 @@ export const useCspBenchmarkIntegrations = ({ return useQuery( [QUERY_KEY, query], - () => http.get>(BENCHMARKS_ROUTE_PATH, { query }), + () => http.get>(POLICIES_ROUTE_PATH, { query }), { keepPreviousData: true } ); }; diff --git a/x-pack/plugins/cloud_defend/public/plugin.tsx b/x-pack/plugins/cloud_defend/public/plugin.tsx index b3db2dcc41ab4..aa9be3d218aa3 100755 --- a/x-pack/plugins/cloud_defend/public/plugin.tsx +++ b/x-pack/plugins/cloud_defend/public/plugin.tsx @@ -53,7 +53,9 @@ export class CloudDefendPlugin implements Plugin - +
+ +
), diff --git a/x-pack/plugins/cloud_defend/public/types.ts b/x-pack/plugins/cloud_defend/public/types.ts index a4a94db0cd6d0..89762b2cdec8e 100755 --- a/x-pack/plugins/cloud_defend/public/types.ts +++ b/x-pack/plugins/cloud_defend/public/types.ts @@ -8,6 +8,11 @@ import type { FleetSetup, FleetStart } from '@kbn/fleet-plugin/public'; import { NewPackagePolicy } from '@kbn/fleet-plugin/public'; import type { ComponentType, ReactNode } from 'react'; +import type { + UsageCollectionSetup, + UsageCollectionStart, +} from '@kbn/usage-collection-plugin/public'; +import type { CloudDefendRouterProps } from './application/router'; import type { CloudDefendPageId } from './common/navigation/types'; /** @@ -16,14 +21,18 @@ import type { CloudDefendPageId } from './common/navigation/types'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CloudDefendPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CloudDefendPluginStart {} +export interface CloudDefendPluginStart { + /** Gets the cloud defend router component for embedding in the security solution. */ + getCloudDefendRouter(): ComponentType; +} export interface CloudDefendPluginSetupDeps { fleet: FleetSetup; + usageCollection?: UsageCollectionSetup; } export interface CloudDefendPluginStartDeps { fleet: FleetStart; + usageCollection?: UsageCollectionStart; } export interface CloudDefendSecuritySolutionContext { diff --git a/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts b/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts new file mode 100644 index 0000000000000..0ca11aa3498cb --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts @@ -0,0 +1,121 @@ +/* + * 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 { map, uniq } from 'lodash'; +import type { SavedObjectsClientContract, Logger } from '@kbn/core/server'; +import type { + AgentPolicyServiceInterface, + AgentService, + PackagePolicyClient, +} from '@kbn/fleet-plugin/server'; +import type { + AgentPolicy, + GetAgentStatusResponse, + ListResult, + PackagePolicy, +} from '@kbn/fleet-plugin/common'; +import { errors } from '@elastic/elasticsearch'; +import { INPUT_CONTROL } from '../../common/constants'; +import { CLOUD_DEFEND_FLEET_PACKAGE_KUERY } from '../../common/utils/helpers'; +import { PoliciesQueryParams } from '../../common/schemas/policy'; + +export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; + +const isFleetMissingAgentHttpError = (error: unknown) => + error instanceof errors.ResponseError && error.statusCode === 404; + +const isPolicyTemplate = (input: any) => input === INPUT_CONTROL; + +const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => { + const integrationNameQuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`; + const kquery = benchmarkFilter + ? `${integrationNameQuery} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}*` + : integrationNameQuery; + + return kquery; +}; + +export type AgentStatusByAgentPolicyMap = Record; + +export const getAgentStatusesByAgentPolicies = async ( + agentService: AgentService, + agentPolicies: AgentPolicy[] | undefined, + logger: Logger +): Promise => { + if (!agentPolicies?.length) return {}; + + const internalAgentService = agentService.asInternalUser; + const result: AgentStatusByAgentPolicyMap = {}; + + try { + for (const agentPolicy of agentPolicies) { + result[agentPolicy.id] = await internalAgentService.getAgentStatusForAgentPolicy( + agentPolicy.id + ); + } + } catch (error) { + if (isFleetMissingAgentHttpError(error)) { + logger.debug('failed to get agent status for agent policy'); + } else { + throw error; + } + } + + return result; +}; + +export const getCloudDefendAgentPolicies = async ( + soClient: SavedObjectsClientContract, + packagePolicies: PackagePolicy[], + agentPolicyService: AgentPolicyServiceInterface +): Promise => + agentPolicyService.getByIds(soClient, uniq(map(packagePolicies, 'policy_id')), { + withPackagePolicies: true, + ignoreMissing: true, + }); + +export const getCloudDefendPackagePolicies = ( + soClient: SavedObjectsClientContract, + packagePolicyService: PackagePolicyClient, + packageName: string, + queryParams: Partial +): Promise> => { + // const sortField = queryParams.sort_field?.replaceAll(POLICIES_PACKAGE_POLICY_PREFIX, ''); + const sortField = queryParams.sort_field; + + return packagePolicyService.list(soClient, { + kuery: getPackageNameQuery(packageName, queryParams.policy_name), + page: queryParams.page, + perPage: queryParams.per_page, + sortField, + sortOrder: queryParams.sort_order, + }); +}; + +export const getInstalledPolicyTemplates = async ( + packagePolicyClient: PackagePolicyClient, + soClient: SavedObjectsClientContract +) => { + try { + // getting all installed cloud_defend package policies + const queryResult = await packagePolicyClient.list(soClient, { + kuery: CLOUD_DEFEND_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_defend/server/routes/policies/policies.ts b/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts index f4a081cf00165..d1142a0d1ab37 100644 --- a/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts +++ b/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts @@ -4,17 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { SavedObjectsClientContract } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { AgentPolicy, PackagePolicy } from '@kbn/fleet-plugin/common'; import { POLICIES_ROUTE_PATH, INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; -import { policiesQueryParamsSchema } from '../../../common/schemas/benchmark'; -import type { Benchmark } from '../../../common/types'; -import { - getBenchmarkFromPackagePolicy, - getBenchmarkTypeFilter, - isNonNullable, -} from '../../../common/utils/helpers'; +import { policiesQueryParamsSchema } from '../../../common/schemas/policy'; +import type { ControlPolicy } from '../../../common/types'; +import { getControlPolicyFromPackagePolicy, isNonNullable } from '../../../common/utils/helpers'; import { CloudDefendRouter } from '../../types'; import { getAgentStatusesByAgentPolicies, @@ -28,35 +23,35 @@ export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; const createPolicies = ( agentPolicies: AgentPolicy[], agentStatusByAgentPolicyId: AgentStatusByAgentPolicyMap, - cspPackagePolicies: PackagePolicy[] -): Promise => { - const cspPackagePoliciesMap = new Map( - cspPackagePolicies.map((packagePolicy) => [packagePolicy.id, packagePolicy]) + cloudDefendPackagePolicies: PackagePolicy[] +): Promise => { + const cloudDefendPackagePoliciesMap = new Map( + cloudDefendPackagePolicies.map((packagePolicy) => [packagePolicy.id, packagePolicy]) ); return Promise.all( agentPolicies.flatMap((agentPolicy) => { - const cspPackagesOnAgent = + const cloudDefendPackagesOnAgent = agentPolicy.package_policies ?.map(({ id: pckPolicyId }) => { - return cspPackagePoliciesMap.get(pckPolicyId); + return cloudDefendPackagePoliciesMap.get(pckPolicyId); }) .filter(isNonNullable) ?? []; - const benchmarks = cspPackagesOnAgent.map(async (cspPackage) => { - const benchmarkId = getBenchmarkFromPackagePolicy(cspPackage.inputs); + const policies = cloudDefendPackagesOnAgent.map(async (cloudDefendPackage) => { + const policyId = getControlPolicyFromPackagePolicy(cloudDefendPackage.inputs); const agentPolicyStatus = { id: agentPolicy.id, name: agentPolicy.name, agents: agentStatusByAgentPolicyId[agentPolicy.id]?.total, }; return { - package_policy: cspPackage, + package_policy: cloudDefendPackage, agent_policy: agentPolicyStatus, }; }); - return benchmarks; + return policies; }) ); }; @@ -98,7 +93,6 @@ export const defineGetPoliciesRoute = (router: CloudDefendRouter): void => ); const policies = await createPolicies( - cloudDefendContext.soClient, agentPolicies, agentStatusesByAgentPolicyId, cloudDefendPackagePolicies.items diff --git a/x-pack/plugins/cloud_defend/server/types.ts b/x-pack/plugins/cloud_defend/server/types.ts index 2d3431fcfdb5b..f68cf28e317c0 100644 --- a/x-pack/plugins/cloud_defend/server/types.ts +++ b/x-pack/plugins/cloud_defend/server/types.ts @@ -58,7 +58,7 @@ export type CloudDefendRequestHandlerContext = CustomRequestHandlerContext<{ }>; /** - * Convenience type for routers in Csp that includes the CspRequestHandlerContext type + * Convenience type for routers in cloud_defend that includes the CloudDefendRequestHandlerContext type * @internal */ export type CloudDefendRouter = IRouter; diff --git a/x-pack/plugins/cloud_defend/tsconfig.json b/x-pack/plugins/cloud_defend/tsconfig.json index 2515fd96e95bc..741a954da0331 100755 --- a/x-pack/plugins/cloud_defend/tsconfig.json +++ b/x-pack/plugins/cloud_defend/tsconfig.json @@ -11,7 +11,8 @@ "@kbn/data-plugin", "@kbn/kibana-react-plugin", "@kbn/monaco", - "@kbn/i18n" + "@kbn/i18n", + "@kbn/usage-collection-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index c4ebcd19eb082..3e13d9c323bae 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -91,6 +91,11 @@ export enum SecurityPageName { cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard', cloudSecurityPostureFindings = 'cloud_security_posture-findings', cloudSecurityPostureRules = 'cloud_security_posture-rules', + /* + * Warning: Computed values are not permitted in an enum with string valued members + * All cloud defend page names must match `CloudDefendPageId` in x-pack/plugins/cloud_defend/public/common/navigation/types.ts + */ + cloudDefendPolicies = 'cloud_defend-policies', dashboardsLanding = 'dashboards', dataQuality = 'data_quality', detections = 'detections', diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index ec88134dab44f..77d9f5c975af9 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -14,6 +14,7 @@ "cases", "cloud", "cloudSecurityPosture", + "cloudDefend", "dashboard", "data", "ecsDataQualityDashboard", diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 9853c52502fb2..b4dcc3a4a2b7b 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; -import { getSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public'; +import { getSecuritySolutionLink as getCloudDefendSecuritySolutionLink } from '@kbn/cloud-defend-plugin/public'; +import { getSecuritySolutionLink as getCloudPostureSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public'; import { getSecuritySolutionDeepLink } from '@kbn/threat-intelligence-plugin/public'; import type { LicenseType } from '@kbn/licensing-plugin/common/types'; import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; @@ -167,7 +168,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ ], }, { - ...getSecuritySolutionLink('dashboard'), + ...getCloudPostureSecuritySolutionLink('dashboard'), features: [FEATURE.general], }, { @@ -251,7 +252,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ ], }, { - ...getSecuritySolutionLink('findings'), + ...getCloudPostureSecuritySolutionLink('findings'), features: [FEATURE.general], navLinkStatus: AppNavLinkStatus.visible, order: 9002, @@ -529,7 +530,10 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ path: RESPONSE_ACTIONS_HISTORY_PATH, }, { - ...getSecuritySolutionLink('benchmarks'), + ...getCloudPostureSecuritySolutionLink('benchmarks'), + }, + { + ...getCloudDefendSecuritySolutionLink('benchmarks'), }, ], }, diff --git a/x-pack/plugins/security_solution/public/cloud_defend/index.ts b/x-pack/plugins/security_solution/public/cloud_defend/index.ts index 798658b15cd42..4ec2329d36bd5 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/index.ts +++ b/x-pack/plugins/security_solution/public/cloud_defend/index.ts @@ -8,7 +8,7 @@ import type { SecuritySubPlugin } from '../app/types'; import { routes } from './routes'; -export class CloudSecurityPosture { +export class CloudDefend { public setup() {} public start(): SecuritySubPlugin { diff --git a/x-pack/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/plugins/security_solution/public/cloud_defend/links.ts index def3b0ed9f5eb..0a92946cf929d 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_defend/links.ts @@ -4,10 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { getSecuritySolutionLink } from '@kbn/cloud-security-posture-plugin/public'; +import { getSecuritySolutionLink } from '@kbn/cloud-defend-plugin/public'; import { i18n } from '@kbn/i18n'; import { SecurityPageName, SERVER_APP_ID } from '../../common/constants'; -import cloudSecurityPostureDashboardImage from '../common/images/cloud_security_posture_dashboard_page.png'; import type { LinkCategories, LinkItem } from '../common/links/types'; import { IconExceptionLists } from '../management/icons/exception_lists'; @@ -16,41 +15,22 @@ const commonLinkProperties: Partial = { capabilities: [`${SERVER_APP_ID}.show`], }; -export const rootLinks: LinkItem = { - ...getSecuritySolutionLink('findings'), - globalNavPosition: 3, - ...commonLinkProperties, -}; - -export const dashboardLinks: LinkItem = { - ...getSecuritySolutionLink('dashboard'), - description: i18n.translate( - 'xpack.securitySolution.appLinks.cloudSecurityPostureDashboardDescription', - { - defaultMessage: 'An overview of findings across all CSP integrations.', - } - ), - landingImage: cloudSecurityPostureDashboardImage, - ...commonLinkProperties, -}; - export const manageLinks: LinkItem = { - ...getSecuritySolutionLink('benchmarks'), - description: i18n.translate( - 'xpack.securitySolution.appLinks.cloudSecurityPostureBenchmarksDescription', - { - defaultMessage: 'View benchmark rules.', - } - ), + ...getSecuritySolutionLink('policies'), + description: i18n.translate('xpack.securitySolution.appLinks.cloudDefendPoliciesDescription', { + defaultMessage: 'View control policies.', + }), landingIcon: IconExceptionLists, ...commonLinkProperties, }; +console.log(manageLinks); + export const manageCategories: LinkCategories = [ { - label: i18n.translate('xpack.securitySolution.appLinks.category.cloudSecurityPosture', { - defaultMessage: 'CLOUD SECURITY POSTURE', + label: i18n.translate('xpack.securitySolution.appLinks.category.cloudDefend', { + defaultMessage: 'DEFEND FOR CONTAINERS (D4C)', }), - linkIds: [SecurityPageName.cloudSecurityPostureBenchmarks], + linkIds: [SecurityPageName.cloudDefendPolicies], }, ]; diff --git a/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx b/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx index f11bac97dfcf0..4afbc47030810 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx +++ b/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import type { CloudSecurityPosturePageId } from '@kbn/cloud-security-posture-plugin/public'; -import { - CLOUD_SECURITY_POSTURE_BASE_PATH, - type CspSecuritySolutionContext, -} from '@kbn/cloud-security-posture-plugin/public'; +import type { + CloudDefendPageId, + CloudDefendSecuritySolutionContext, +} from '@kbn/cloud-defend-plugin/public'; +import { CLOUD_DEFEND_BASE_PATH } from '@kbn/cloud-defend-plugin/public'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import type { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; import { useKibana } from '../common/lib/kibana'; @@ -20,35 +20,35 @@ import { FiltersGlobal } from '../common/components/filters_global'; import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; // This exists only for the type signature cast -const CloudPostureSpyRoute = ({ pageName, ...rest }: { pageName?: CloudSecurityPosturePageId }) => ( +const CloudDefendSpyRoute = ({ pageName, ...rest }: { pageName?: CloudDefendPageId }) => ( ); -const cspSecuritySolutionContext: CspSecuritySolutionContext = { +const cloudDefendSecuritySolutionContext: CloudDefendSecuritySolutionContext = { getFiltersGlobalComponent: () => FiltersGlobal, - getSpyRouteComponent: () => CloudPostureSpyRoute, + getSpyRouteComponent: () => CloudDefendSpyRoute, }; -const CloudSecurityPosture = () => { - const { cloudSecurityPosture } = useKibana().services; - const CloudSecurityPostureRouter = cloudSecurityPosture.getCloudSecurityPostureRouter(); +const CloudDefend = () => { + const { cloudDefend } = useKibana().services; + const CloudDefendRouter = cloudDefend.getCloudDefendRouter(); return ( - + - + ); }; -CloudSecurityPosture.displayName = 'CloudSecurityPosture'; +CloudDefend.displayName = 'CloudDefend'; export const routes: SecuritySubPluginRoutes = [ { - path: CLOUD_SECURITY_POSTURE_BASE_PATH, - component: CloudSecurityPosture, + path: CLOUD_DEFEND_BASE_PATH, + component: CloudDefend, }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index b0190d40ced78..29f69700afdb7 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -42,6 +42,7 @@ export type UrlStateType = | 'explore' | 'dashboards' | 'indicators' + | 'cloud_defend' | 'cloud_posture' | 'findings' | 'entity_analytics' @@ -84,6 +85,7 @@ export const securityNavKeys = [ SecurityPageName.cloudSecurityPostureDashboard, SecurityPageName.cloudSecurityPostureFindings, SecurityPageName.cloudSecurityPostureBenchmarks, + SecurityPageName.cloudDefendPolicies, SecurityPageName.entityAnalytics, SecurityPageName.dataQuality, ] as const; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 65b43d3d4635b..cb16591da9fb4 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -33,6 +33,7 @@ import type { LicensingPluginStart, LicensingPluginSetup } from '@kbn/licensing- import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { CloudDefendPluginStart } from '@kbn/cloud-defend-plugin/public'; import type { CspClientPluginStart } from '@kbn/cloud-security-posture-plugin/public'; import type { ApmBase } from '@elastic/apm-rum'; import type { @@ -55,6 +56,7 @@ import type { Timelines } from './timelines'; import type { Management } from './management'; import type { LandingPages } from './landing_pages'; import type { CloudSecurityPosture } from './cloud_security_posture'; +import type { CloudDefend } from './cloud_defend'; import type { ThreatIntelligence } from './threat_intelligence'; import type { SecuritySolutionTemplateWrapper } from './app/home/template_wrapper'; import type { Explore } from './explore'; @@ -91,6 +93,7 @@ export interface StartPlugins { dataViewFieldEditor: IndexPatternFieldEditorStart; osquery?: OsqueryPluginStart; security: SecurityPluginStart; + cloudDefend: CloudDefendPluginStart; cloudSecurityPosture: CspClientPluginStart; threatIntelligence: ThreatIntelligencePluginStart; cloudExperiments?: CloudExperimentsPluginStart; @@ -142,6 +145,7 @@ export interface SubPlugins { timelines: Timelines; management: Management; landingPages: LandingPages; + cloudDefend: CloudDefend; cloudSecurityPosture: CloudSecurityPosture; threatIntelligence: ThreatIntelligence; } @@ -158,6 +162,7 @@ export interface StartedSubPlugins { timelines: ReturnType; management: ReturnType; landingPages: ReturnType; + cloudDefend: ReturnType; cloudSecurityPosture: ReturnType; threatIntelligence: ReturnType; } diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 9d312e57f98af..521812319c4c3 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -35,6 +35,7 @@ "@kbn/actions-plugin", "@kbn/alerting-plugin", "@kbn/cases-plugin", + "@kbn/cloud-defend-plugin", "@kbn/cloud-experiments-plugin", "@kbn/cloud-security-posture-plugin", "@kbn/encrypted-saved-objects-plugin", From 494f0d8a5457ebc04cf6a5d8ef8c085ce9e9fafe Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Wed, 15 Feb 2023 23:52:00 +0000 Subject: [PATCH 03/19] policies page working, empty states working, docs link and add/edit links working --- .../plugins/cloud_defend/common/constants.ts | 6 + x-pack/plugins/cloud_defend/common/types.ts | 6 +- .../cloud_defend/common/utils/helpers.test.ts | 63 --- .../cloud_defend/common/utils/helpers.ts | 37 -- x-pack/plugins/cloud_defend/kibana.jsonc | 22 +- .../cloud_defend/public/assets/icons/logo.svg | 13 + .../public/common/api/use_setup_status_api.ts | 14 +- .../navigation/security_solution_links.ts | 12 +- .../use_cloud_defend_integration_link.ts | 28 +- .../components/cloud_defend_page/index.tsx | 109 ++-- .../components/policies_table/index.tsx | 42 +- .../public/pages/policies/index.tsx | 30 +- .../policies/use_cloud_defend_policies.ts | 6 +- .../server/lib/check_index_status.ts | 35 ++ .../cloud_defend/server/lib/fleet_util.ts | 8 +- x-pack/plugins/cloud_defend/server/plugin.ts | 12 +- .../server/routes/policies/policies.ts | 7 +- .../server/routes/setup_routes.ts | 4 +- .../server/routes/status/status.test.ts | 492 ++++++++++++++++++ .../server/routes/status/status.ts | 213 ++++++++ x-pack/plugins/cloud_defend/server/types.ts | 3 +- x-pack/plugins/cloud_defend/tsconfig.json | 8 +- x-pack/plugins/security_solution/kibana.jsonc | 1 + .../public/app/deep_links/index.ts | 2 +- .../public/cloud_defend/links.ts | 8 +- .../navigation/breadcrumbs/index.ts | 3 + .../public/common/links/app_links.ts | 2 + .../public/lazy_sub_plugins.tsx | 2 + .../security_solution/public/plugin.tsx | 2 + 29 files changed, 940 insertions(+), 250 deletions(-) delete mode 100644 x-pack/plugins/cloud_defend/common/utils/helpers.test.ts create mode 100644 x-pack/plugins/cloud_defend/public/assets/icons/logo.svg create mode 100644 x-pack/plugins/cloud_defend/server/lib/check_index_status.ts create mode 100644 x-pack/plugins/cloud_defend/server/routes/status/status.test.ts create mode 100644 x-pack/plugins/cloud_defend/server/routes/status/status.ts diff --git a/x-pack/plugins/cloud_defend/common/constants.ts b/x-pack/plugins/cloud_defend/common/constants.ts index 5adca73186186..8fc54773da295 100755 --- a/x-pack/plugins/cloud_defend/common/constants.ts +++ b/x-pack/plugins/cloud_defend/common/constants.ts @@ -4,10 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; export const PLUGIN_ID = 'cloudDefend'; export const PLUGIN_NAME = 'Cloud Defend'; export const INTEGRATION_PACKAGE_NAME = 'cloud_defend'; export const INPUT_CONTROL = 'cloud_defend/control'; export const ALERTS_DATASET = 'cloud_defend.alerts'; +export const ALERTS_INDEX_PATTERN = 'cloud_defend.alerts*'; + export const POLICIES_ROUTE_PATH = '/internal/cloud_defend/policies'; +export const STATUS_ROUTE_PATH = '/internal/cloud_defend/status'; + +export const CLOUD_DEFEND_FLEET_PACKAGE_KUERY = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${INTEGRATION_PACKAGE_NAME}`; diff --git a/x-pack/plugins/cloud_defend/common/types.ts b/x-pack/plugins/cloud_defend/common/types.ts index 8d3abad0ab202..5f2c68576049b 100644 --- a/x-pack/plugins/cloud_defend/common/types.ts +++ b/x-pack/plugins/cloud_defend/common/types.ts @@ -18,7 +18,7 @@ export type CloudDefendStatusCode = | 'unprivileged' // user lacks privileges for the latest findings index | 'index-timeout' // index timeout was surpassed since installation | 'not-deployed' // no healthy agents were deployed - | 'not-installed'; // number of installed csp integrations is 0; + | 'not-installed'; // number of installed integrations is 0; export interface IndexDetails { index: string; @@ -30,8 +30,6 @@ interface BaseCloudDefendSetupStatus { latestPackageVersion: string; installedPackagePolicies: number; healthyAgents: number; - isPluginInitialized: boolean; - // installedPolicyTemplates: PosturePolicyTemplate[]; } interface CloudDefendSetupNotInstalledStatus extends BaseCloudDefendSetupStatus { @@ -51,7 +49,7 @@ export type CloudDefendSetupStatus = export type AgentPolicyStatus = Pick & { agents: number }; -export interface ControlPolicy { +export interface CloudDefendPolicy { package_policy: PackagePolicy; agent_policy: AgentPolicyStatus; } diff --git a/x-pack/plugins/cloud_defend/common/utils/helpers.test.ts b/x-pack/plugins/cloud_defend/common/utils/helpers.test.ts deleted file mode 100644 index 24298518aef05..0000000000000 --- a/x-pack/plugins/cloud_defend/common/utils/helpers.test.ts +++ /dev/null @@ -1,63 +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 { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; -import { getBenchmarkFromPackagePolicy, getBenchmarkTypeFilter } from './helpers'; - -describe('test helper methods', () => { - it('get default integration type from inputs with multiple enabled types', () => { - const mockPackagePolicy = createPackagePolicyMock(); - - // Both enabled falls back to default - mockPackagePolicy.inputs = [ - { type: 'cloudbeat/cis_k8s', enabled: true, streams: [] }, - { type: 'cloudbeat/cis_eks', enabled: true, streams: [] }, - ]; - const type = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); - expect(type).toMatch('cis_k8s'); - }); - - it('get default integration type from inputs without any enabled types', () => { - const mockPackagePolicy = createPackagePolicyMock(); - - // None enabled falls back to default - mockPackagePolicy.inputs = [ - { type: 'cloudbeat/cis_k8s', enabled: false, streams: [] }, - { type: 'cloudbeat/cis_eks', enabled: false, streams: [] }, - ]; - const type = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); - expect(type).toMatch('cis_k8s'); - }); - - it('get EKS integration type', () => { - const mockPackagePolicy = createPackagePolicyMock(); - - // Single EKS selected - mockPackagePolicy.inputs = [ - { type: 'cloudbeat/cis_eks', enabled: true, streams: [] }, - { type: 'cloudbeat/cis_k8s', enabled: false, streams: [] }, - ]; - const typeEks = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); - expect(typeEks).toMatch('cis_eks'); - }); - - it('get Vanilla K8S integration type', () => { - const mockPackagePolicy = createPackagePolicyMock(); - - // Single k8s selected - mockPackagePolicy.inputs = [ - { type: 'cloudbeat/cis_eks', enabled: false, streams: [] }, - { type: 'cloudbeat/cis_k8s', enabled: true, streams: [] }, - ]; - const typeK8s = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); - expect(typeK8s).toMatch('cis_k8s'); - }); - it('get benchmark type filter based on a benchmark id', () => { - const typeFilter = getBenchmarkTypeFilter('cis_eks'); - expect(typeFilter).toMatch('csp-rule-template.attributes.metadata.benchmark.id: "cis_eks"'); - }); -}); diff --git a/x-pack/plugins/cloud_defend/common/utils/helpers.ts b/x-pack/plugins/cloud_defend/common/utils/helpers.ts index 64dc2b7d745fd..2fdc1087e54d2 100644 --- a/x-pack/plugins/cloud_defend/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_defend/common/utils/helpers.ts @@ -6,16 +6,6 @@ */ import { Truthy } from 'lodash'; -import { - NewPackagePolicy, - NewPackagePolicyInput, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, - PackagePolicy, - PackagePolicyInput, -} from '@kbn/fleet-plugin/common'; -import { INTEGRATION_PACKAGE_NAME } from '../constants'; -import type { PolicyId } from '../types'; -import { INPUT_CONTROL } from '../constants'; /** * @example @@ -34,33 +24,6 @@ export const extractErrorMessage = (e: unknown, defaultMessage = 'Unknown Error' return defaultMessage; // TODO: i18n }; -export const isEnabledControlInputType = (input: PackagePolicyInput | NewPackagePolicyInput) => - input.enabled; - -export const isCloudDefendPackage = (packageName?: string) => - packageName === INTEGRATION_PACKAGE_NAME; - -export const getControlPolicyFromPackagePolicy = ( - inputs: PackagePolicy['inputs'] | NewPackagePolicy['inputs'] -): PolicyId => { - const enabledInputs = inputs.filter(isEnabledControlInputType); - - // Use the only enabled input - if (enabledInputs.length === 1) { - return getInputType(enabledInputs[0].type); - } - - // Use the default benchmark id for multiple/none selected - return getInputType(INPUT_CONTROL); -}; - -const getInputType = (inputType: string): string => { - // Get the last part of the input type, input type structure: cloudbeat/ - return inputType.split('/')[1]; -}; - -export const CLOUD_DEFEND_FLEET_PACKAGE_KUERY = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${INTEGRATION_PACKAGE_NAME}`; - export function assert(condition: any, msg?: string): asserts condition { if (!condition) { throw new Error(msg); diff --git a/x-pack/plugins/cloud_defend/kibana.jsonc b/x-pack/plugins/cloud_defend/kibana.jsonc index f561c33a5f832..ac66730bd6993 100644 --- a/x-pack/plugins/cloud_defend/kibana.jsonc +++ b/x-pack/plugins/cloud_defend/kibana.jsonc @@ -2,14 +2,30 @@ "type": "plugin", "id": "@kbn/cloud-defend-plugin", "owner": "@elastic/sec-cloudnative-integrations", - "description": "Defend for Containers", + "description": "Defend for containers (D4C)", "plugin": { "id": "cloudDefend", - "server": false, + "server": true, "browser": true, + "configPath": [ + "xpack", + "cloudDefend" + ], "requiredPlugins": [ + "navigation", + "data", "fleet", - "kibanaReact" + "unifiedSearch", + "kibanaReact", + "cloud", + "security" + ], + "optionalPlugins": [ + "usageCollection" + ], + "requiredBundles": [ + "kibanaReact", + "usageCollection" ] } } diff --git a/x-pack/plugins/cloud_defend/public/assets/icons/logo.svg b/x-pack/plugins/cloud_defend/public/assets/icons/logo.svg new file mode 100644 index 0000000000000..a0534292eb717 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/assets/icons/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts index 7c5d4eb8dc31b..f65926ec6c98a 100644 --- a/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts +++ b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts @@ -7,18 +7,18 @@ import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; import { useKibana } from '../hooks/use_kibana'; -import { CspSetupStatus } from '../../../common/types'; +import { CloudDefendSetupStatus } from '../../../common/types'; import { STATUS_ROUTE_PATH } from '../../../common/constants'; -const getCspSetupStatusQueryKey = 'csp_status_key'; +const getCloudDefendSetupStatusQueryKey = 'cloud_defend_status_key'; -export const useCspSetupStatusApi = ({ +export const useCloudDefendSetupStatusApi = ({ options, -}: { options?: UseQueryOptions } = {}) => { +}: { options?: UseQueryOptions } = {}) => { const { http } = useKibana().services; - return useQuery( - [getCspSetupStatusQueryKey], - () => http.get(STATUS_ROUTE_PATH), + return useQuery( + [getCloudDefendSetupStatusQueryKey], + () => http.get(STATUS_ROUTE_PATH), options ); }; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts index 863630770750f..0ef838437abf3 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts @@ -27,11 +27,13 @@ interface CloudDefendNavTab { */ export const getSecuritySolutionLink = ( cloudDefendPage: CloudDefendPage -): CloudDefendLinkItem => ({ - id: cloudDefendPages[cloudDefendPage].id as TId, - title: cloudDefendPages[cloudDefendPage].name, - path: cloudDefendPages[cloudDefendPage].path, -}); +): CloudDefendLinkItem => { + return { + id: cloudDefendPages[cloudDefendPage].id as TId, + title: cloudDefendPages[cloudDefendPage].name, + path: cloudDefendPages[cloudDefendPage].path, + }; +}; /** * Gets the link properties of a Cloud Defend page for navigation in the old security solution navigation. diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts b/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts index 6355346647d7e..b2d0cca31fcba 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts @@ -10,13 +10,20 @@ import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; import { useCloudDefendIntegration } from '../api/use_cloud_defend_integration'; import { useKibana } from '../hooks/use_kibana'; -export const useCloudDefendIntegrationLink = (): string | undefined => { +export const useCloudDefendIntegrationLinks = (): { + addIntegrationLink: string | undefined; + docsLink: string; +} => { const { http } = useKibana().services; const cloudDefendIntegration = useCloudDefendIntegration(); - if (!cloudDefendIntegration.isSuccess) return; + if (!cloudDefendIntegration.isSuccess) + return { + addIntegrationLink: undefined, + docsLink: 'https://www.elastic.co/guide/index.html', + }; - const path = pagePathGetters + const addIntegrationLink = pagePathGetters .add_integration_to_policy({ integration: INTEGRATION_PACKAGE_NAME, pkgkey: pkgKeyFromPackageInfo({ @@ -26,5 +33,18 @@ export const useCloudDefendIntegrationLink = (): string | undefined => { }) .join(''); - return http.basePath.prepend(path); + const docsLink = pagePathGetters + .integration_details_overview({ + integration: INTEGRATION_PACKAGE_NAME, + pkgkey: pkgKeyFromPackageInfo({ + name: cloudDefendIntegration.data.item.name, + version: cloudDefendIntegration.data.item.version, + }), + }) + .join(''); + + return { + addIntegrationLink: http.basePath.prepend(addIntegrationLink), + docsLink: http.basePath.prepend(docsLink), + }; }; diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx index 50fbcb8661e9c..b8526bd2cfdb5 100644 --- a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx @@ -21,19 +21,17 @@ import { css } from '@emotion/react'; // import { SubscriptionNotAllowed } from './subscription_not_allowed'; // import { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; import { FullSizeCenteredPage } from '../full_size_page'; -// import { useCloudDefendSetupStatusApi } from '../common/api/use_setup_status_api'; -import { CloudDefendLoadingState } from '../loading_state'; -// import { useCloudDefendIntegrationLink } from '../common/navigation/use_cloud_defend_integration_link'; +import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api'; +import { LoadingState } from '../loading_state'; +import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_link'; -// import noDataIllustration from '../assets/illustrations/no_data_illustration.svg'; +import noDataIllustration from '../../assets/icons/logo.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'; +export const LOADING_STATE_TEST_SUBJECT = 'cloud_defend_page_loading'; +export const ERROR_STATE_TEST_SUBJECT = 'cloud_defend_page_error'; +export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_defend_page_package_not_installed'; +export const DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_defend_page_no_data'; +export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_defend_page_subscription_not_allowed'; interface CommonError { body: { @@ -84,9 +82,12 @@ export const CloudDefendNoDataPage = ({ } `} pageTitle={pageTitle} - solution={i18n.translate('xpack.csp.cloudPosturePage.packageNotInstalled.solutionNameLabel', { - defaultMessage: 'Cloud Security Posture', - })} + solution={i18n.translate( + 'xpack.cloudDefend.cloudDefendPage.packageNotInstalled.solutionNameLabel', + { + defaultMessage: 'Defend for containers (D4C)', + } + )} docsLink={docsLink} logo="logoSecurity" actions={{ @@ -102,22 +103,22 @@ export const CloudDefendNoDataPage = ({ }; const packageNotInstalledRenderer = ({ - kspmIntegrationLink, - cspmIntegrationLink, + addIntegrationLink, + docsLink, }: { - kspmIntegrationLink?: string; - cspmIntegrationLink?: string; + addIntegrationLink?: string; + docsLink?: string; }) => { return ( } + icon={} title={

} @@ -126,15 +127,14 @@ const packageNotInstalledRenderer = ({ body={

+ ), @@ -145,18 +145,10 @@ const packageNotInstalledRenderer = ({ actions={ - + - - - - - @@ -168,12 +160,12 @@ const packageNotInstalledRenderer = ({ }; const defaultLoadingRenderer = () => ( - + - + ); const defaultErrorRenderer = (error: unknown) => ( @@ -185,8 +177,8 @@ const defaultErrorRenderer = (error: unknown) => ( title={

} @@ -194,7 +186,7 @@ const defaultErrorRenderer = (error: unknown) => ( isCommonError(error) ? (

( ); -const defaultNoDataRenderer = () => ( +const defaultNoDataRenderer = (docsLink: string) => ( @@ -231,14 +225,14 @@ const defaultNoDataRenderer = () => ( -);*/ +); */ interface CloudPosturePageProps { children: React.ReactNode; query?: UseQueryResult; loadingRender?: () => React.ReactNode; errorRender?: (error: TError) => React.ReactNode; - noDataRenderer?: () => React.ReactNode; + noDataRenderer?: (docsLink: string) => React.ReactNode; } export const CloudDefendPage = ({ @@ -248,11 +242,12 @@ export const CloudDefendPage = ({ errorRender = defaultErrorRenderer, noDataRenderer = defaultNoDataRenderer, }: CloudPosturePageProps) => { - const subscriptionStatus = useSubscriptionStatus(); + // const subscriptionStatus = useSubscriptionStatus(); const getSetupStatus = useCloudDefendSetupStatusApi(); - const integrationLink = useCloudDefendIntegrationLink(CONTROL_POLICY_TEMPLATE); + const { addIntegrationLink, docsLink } = useCloudDefendIntegrationLinks(); const render = () => { + // TODO: subscription status work.. /* if (subscriptionStatus.isError) { return defaultErrorRenderer(subscriptionStatus.error); } @@ -274,7 +269,7 @@ export const CloudDefendPage = ({ } if (getSetupStatus.data.status === 'not-installed') { - return packageNotInstalledRenderer({ kspmIntegrationLink, cspmIntegrationLink }); + return packageNotInstalledRenderer({ addIntegrationLink, docsLink }); } if (!query) { @@ -290,7 +285,7 @@ export const CloudDefendPage = ({ } if (!query.data) { - return noDataRenderer(); + return noDataRenderer(docsLink); } return children; diff --git a/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx b/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx index 39a5421ba653f..00f487cac8c17 100644 --- a/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx +++ b/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx @@ -18,18 +18,18 @@ import { generatePath } from 'react-router-dom'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { i18n } from '@kbn/i18n'; import { TimestampTableCell } from '../timestamp_table_cell'; -import type { ControlPolicy } from '../../../common/types'; +import type { CloudDefendPolicy } from '../../../common/types'; import { useKibana } from '../../common/hooks/use_kibana'; import * as TEST_SUBJ from '../../pages/policies/test_subjects'; interface PoliciesTableProps extends Pick< - EuiBasicTableProps, + EuiBasicTableProps, 'loading' | 'error' | 'noItemsMessage' | 'sorting' >, Pagination { - policies: ControlPolicy[]; - setQuery(pagination: CriteriaWithPagination): void; + policies: CloudDefendPolicy[]; + setQuery(pagination: CriteriaWithPagination): void; 'data-test-subj'?: string; } @@ -49,23 +49,17 @@ const IntegrationButtonLink = ({ packagePolicyId: string; policyId: string; }) => { - const { application } = useKibana().services; + const editIntegrationLink = pagePathGetters + .edit_integration({ + packagePolicyId, + policyId, + }) + .join(''); - return ( - - {packageName} - - ); + return {packageName}; }; -const POLICIES_TABLE_COLUMNS: Array> = [ +const POLICIES_TABLE_COLUMNS: Array> = [ { field: 'package_policy.name', name: i18n.translate('xpack.cloudDefend.policies.policiesTable.integrationNameColumnTitle', { @@ -82,14 +76,6 @@ const POLICIES_TABLE_COLUMNS: Array> = [ sortable: true, 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.INTEGRATION_NAME, }, - { - field: 'rules_count', - name: i18n.translate('xpack.cloudDefend.policies.policiesTable.rulesColumnTitle', { - defaultMessage: 'Rules', - }), - truncateText: true, - 'data-test-subj': TEST_SUBJ.POLICIES_TABLE_COLUMNS.RULES, - }, { field: 'agent_policy.name', name: i18n.translate('xpack.cloudDefend.policies.policiesTable.agentPolicyColumnTitle', { @@ -124,7 +110,7 @@ const POLICIES_TABLE_COLUMNS: Array> = [ }), dataType: 'date', truncateText: true, - render: (timestamp: ControlPolicy['package_policy']['created_at']) => ( + render: (timestamp: CloudDefendPolicy['package_policy']['created_at']) => ( ), sortable: true, @@ -150,7 +136,7 @@ export const PoliciesTable = ({ totalItemCount, }; - const onChange = ({ page, sort }: CriteriaWithPagination) => { + const onChange = ({ page, sort }: CriteriaWithPagination) => { setQuery({ page: { ...page, index: page.index + 1 }, sort }); }; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx index a8ff9c7c01f98..4aa2e29541b79 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx @@ -20,8 +20,6 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; -import { pagePathGetters } from '@kbn/fleet-plugin/public'; -import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; import { CloudDefendPageTitle } from '../../components/cloud_defend_page_title'; import { CloudDefendPage } from '../../components/cloud_defend_page'; import { PoliciesTable } from '../../components/policies_table'; @@ -30,28 +28,22 @@ import { extractErrorMessage } from '../../../common/utils/helpers'; import * as TEST_SUBJ from './test_subjects'; import { LOCAL_STORAGE_PAGE_SIZE } from '../../common/constants'; import { usePageSize } from '../../common/hooks/use_page_size'; -import { useKibana } from '../../common/hooks/use_kibana'; +import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_link'; const SEARCH_DEBOUNCE_MS = 300; const AddIntegrationButton = () => { - const { http } = useKibana().services; - - const integrationsPath = pagePathGetters - .integrations_all({ - searchTerm: INTEGRATION_PACKAGE_NAME, - }) - .join(''); + const { addIntegrationLink } = useCloudDefendIntegrationLinks(); return ( @@ -65,12 +57,12 @@ const EmptyState = ({ name }: { name: string }) => ( {name && ( @@ -82,8 +74,8 @@ const EmptyState = ({ name }: { name: string }) => ( @@ -98,7 +90,7 @@ const TotalIntegrationsCount = ({ @@ -122,7 +114,7 @@ const SearchField = ({ onSearch={setLocalValue} isLoading={isLoading} placeholder={i18n.translate( - 'xpack.csp.benchmarks.benchmarkSearchField.searchPlaceholder', + 'xpack.cloudDefend.policies.policySearchField.searchPlaceholder', { defaultMessage: 'Search integration name' } )} incremental diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/use_cloud_defend_policies.ts b/x-pack/plugins/cloud_defend/public/pages/policies/use_cloud_defend_policies.ts index a327e9b878cc8..e0766026e12b8 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/use_cloud_defend_policies.ts +++ b/x-pack/plugins/cloud_defend/public/pages/policies/use_cloud_defend_policies.ts @@ -8,9 +8,9 @@ import { useQuery } from '@tanstack/react-query'; import type { ListResult } from '@kbn/fleet-plugin/common'; import { POLICIES_ROUTE_PATH } from '../../../common/constants'; -import type { PoliciesQueryParams } from '../../../common/schemas/policies'; +import type { PoliciesQueryParams } from '../../../common/schemas/policy'; import { useKibana } from '../../common/hooks/use_kibana'; -import type { Policy } from '../../../common/types'; +import type { CloudDefendPolicy } from '../../../common/types'; const QUERY_KEY = 'cloud_defend_policies'; @@ -40,7 +40,7 @@ export const useCloudDefendPolicies = ({ return useQuery( [QUERY_KEY, query], - () => http.get>(POLICIES_ROUTE_PATH, { query }), + () => http.get>(POLICIES_ROUTE_PATH, { query }), { keepPreviousData: true } ); }; diff --git a/x-pack/plugins/cloud_defend/server/lib/check_index_status.ts b/x-pack/plugins/cloud_defend/server/lib/check_index_status.ts new file mode 100644 index 0000000000000..984c68a76a6b6 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/lib/check_index_status.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 { ElasticsearchClient, type Logger } from '@kbn/core/server'; +import { IndexStatus } from '../../common/types'; + +export const checkIndexStatus = async ( + esClient: ElasticsearchClient, + index: string, + logger: Logger +): Promise => { + try { + const queryResult = await esClient.search({ + index, + query: { + match_all: {}, + }, + size: 1, + }); + + return queryResult.hits.hits.length ? 'not-empty' : 'empty'; + } catch (e) { + logger.debug(e); + if (e?.meta?.body?.error?.type === 'security_exception') { + return 'unprivileged'; + } + + // Assuming index doesn't exist + return 'empty'; + } +}; diff --git a/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts b/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts index 0ca11aa3498cb..85ec23aff2a22 100644 --- a/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts +++ b/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts @@ -18,9 +18,8 @@ import type { PackagePolicy, } from '@kbn/fleet-plugin/common'; import { errors } from '@elastic/elasticsearch'; -import { INPUT_CONTROL } from '../../common/constants'; -import { CLOUD_DEFEND_FLEET_PACKAGE_KUERY } from '../../common/utils/helpers'; -import { PoliciesQueryParams } from '../../common/schemas/policy'; +import { INPUT_CONTROL, CLOUD_DEFEND_FLEET_PACKAGE_KUERY } from '../../common/constants'; +import { POLICIES_PACKAGE_POLICY_PREFIX, PoliciesQueryParams } from '../../common/schemas/policy'; export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; @@ -83,8 +82,7 @@ export const getCloudDefendPackagePolicies = ( packageName: string, queryParams: Partial ): Promise> => { - // const sortField = queryParams.sort_field?.replaceAll(POLICIES_PACKAGE_POLICY_PREFIX, ''); - const sortField = queryParams.sort_field; + const sortField = queryParams.sort_field?.replaceAll(POLICIES_PACKAGE_POLICY_PREFIX, ''); return packagePolicyService.list(soClient, { kuery: getPackageNameQuery(packageName, queryParams.policy_name), diff --git a/x-pack/plugins/cloud_defend/server/plugin.ts b/x-pack/plugins/cloud_defend/server/plugin.ts index 777d6a00becf0..246fc338b3e8c 100644 --- a/x-pack/plugins/cloud_defend/server/plugin.ts +++ b/x-pack/plugins/cloud_defend/server/plugin.ts @@ -9,26 +9,32 @@ import { CloudDefendPluginSetup, CloudDefendPluginStart, CloudDefendPluginStartDeps, + CloudDefendPluginSetupDeps, } from './types'; import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; import { setupRoutes } from './routes/setup_routes'; export class CloudDefendPlugin implements Plugin { private readonly logger: Logger; + private isCloudEnabled?: boolean; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup( + core: CoreSetup, + plugins: CloudDefendPluginSetupDeps + ) { this.logger.debug('cloudDefend: Setup'); - const router = core.http.createRouter(); setupRoutes({ core, logger: this.logger, }); + this.isCloudEnabled = plugins.cloud.isCloudEnabled; + return {}; } @@ -39,8 +45,6 @@ export class CloudDefendPlugin implements Plugin => { +): Promise => { const cloudDefendPackagePoliciesMap = new Map( cloudDefendPackagePolicies.map((packagePolicy) => [packagePolicy.id, packagePolicy]) ); @@ -39,7 +39,6 @@ const createPolicies = ( .filter(isNonNullable) ?? []; const policies = cloudDefendPackagesOnAgent.map(async (cloudDefendPackage) => { - const policyId = getControlPolicyFromPackagePolicy(cloudDefendPackage.inputs); const agentPolicyStatus = { id: agentPolicy.id, name: agentPolicy.name, diff --git a/x-pack/plugins/cloud_defend/server/routes/setup_routes.ts b/x-pack/plugins/cloud_defend/server/routes/setup_routes.ts index 8e9f3e4f0a637..1a9ef57ba83c7 100644 --- a/x-pack/plugins/cloud_defend/server/routes/setup_routes.ts +++ b/x-pack/plugins/cloud_defend/server/routes/setup_routes.ts @@ -14,7 +14,7 @@ import type { } from '../types'; import { PLUGIN_ID } from '../../common/constants'; import { defineGetPoliciesRoute } from './policies/policies'; -// import { defineGetCloudDefendStatusRoute } from './status/status'; +import { defineGetCloudDefendStatusRoute } from './status/status'; /** * 1. Registers routes @@ -29,7 +29,7 @@ export function setupRoutes({ }) { const router = core.http.createRouter(); defineGetPoliciesRoute(router); - // defineGetCloudDefendStatusRoute(router); + defineGetCloudDefendStatusRoute(router); core.http.registerRouteHandlerContext( PLUGIN_ID, diff --git a/x-pack/plugins/cloud_defend/server/routes/status/status.test.ts b/x-pack/plugins/cloud_defend/server/routes/status/status.test.ts new file mode 100644 index 0000000000000..98c6536c277d3 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/routes/status/status.test.ts @@ -0,0 +1,492 @@ +/* + * 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 { defineGetCspStatusRoute, INDEX_TIMEOUT_IN_MINUTES } from './status'; +import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; +import type { ESSearchResponse } from '@kbn/es-types'; +import { + AgentClient, + AgentPolicyServiceInterface, + AgentService, + PackageClient, + PackagePolicyClient, + PackageService, +} from '@kbn/fleet-plugin/server'; +import { + AgentPolicy, + GetAgentStatusResponse, + Installation, + RegistryPackage, +} from '@kbn/fleet-plugin/common'; +import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; +import { createCspRequestHandlerContextMock } from '../../mocks'; +import { errors } from '@elastic/elasticsearch'; + +const mockCspPackageInfo: Installation = { + verification_status: 'verified', + installed_kibana: [], + installed_kibana_space_id: 'default', + installed_es: [], + package_assets: [], + es_index_patterns: { findings: 'logs-cloud_security_posture.findings-*' }, + name: 'cloud_security_posture', + version: '0.0.14', + install_version: '0.0.14', + install_status: 'installed', + install_started_at: '2022-06-16T15:24:58.281Z', + install_source: 'registry', +}; + +const mockLatestCspPackageInfo: RegistryPackage = { + format_version: 'mock', + name: 'cloud_security_posture', + title: 'CIS Kubernetes Benchmark', + version: '0.0.14', + release: 'experimental', + description: 'Check Kubernetes cluster compliance with the Kubernetes CIS benchmark.', + type: 'integration', + download: '/epr/cloud_security_posture/cloud_security_posture-0.0.14.zip', + path: '/package/cloud_security_posture/0.0.14', + policy_templates: [], + owner: { github: 'elastic/cloud-security-posture' }, + categories: ['containers', 'kubernetes'], +}; + +describe('CspSetupStatus route', () => { + const router = httpServiceMock.createRouter(); + let mockContext: ReturnType; + let mockPackagePolicyService: jest.Mocked; + let mockAgentPolicyService: jest.Mocked; + let mockAgentService: jest.Mocked; + let mockAgentClient: jest.Mocked; + let mockPackageService: PackageService; + let mockPackageClient: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + + mockContext = createCspRequestHandlerContextMock(); + mockPackagePolicyService = mockContext.csp.packagePolicyService; + mockAgentPolicyService = mockContext.csp.agentPolicyService; + mockAgentService = mockContext.csp.agentService; + mockPackageService = mockContext.csp.packageService; + + mockAgentClient = mockAgentService.asInternalUser as jest.Mocked; + mockPackageClient = mockPackageService.asInternalUser as jest.Mocked; + }); + + it('validate the API route path', async () => { + defineGetCspStatusRoute(router); + const [config, _] = router.get.mock.calls[0]; + + expect(config.path).toEqual('/internal/cloud_security_posture/status'); + }); + + const indices = [ + { + index: 'logs-cloud_security_posture.findings-default*', + expected_status: 'not-installed', + }, + { + index: 'logs-cloud_security_posture.findings_latest-default', + expected_status: 'unprivileged', + }, + { + index: 'logs-cloud_security_posture.scores-default', + expected_status: 'unprivileged', + }, + ]; + + indices.forEach((idxTestCase) => { + it( + 'Verify the API result when there are no permissions to index: ' + idxTestCase.index, + async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseImplementation( + (req) => { + if (req?.index === idxTestCase.index) { + throw new errors.ResponseError({ + body: { + error: { + type: 'security_exception', + }, + }, + statusCode: 503, + headers: {}, + warnings: [], + meta: {} as any, + }); + } + + return { + hits: { + hits: [{}], + }, + } as any; + } + ); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 0, + page: 1, + perPage: 100, + }); + + // Act + defineGetCspStatusRoute(router); + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + await handler(mockContext, mockRequest, mockResponse); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]?.body; + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: idxTestCase.expected_status, + }); + } + ); + }); + + it('Verify the API result when there are findings and no installed policies', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ + hits: { + hits: [{ Findings: 'foo' }], + }, + } as unknown as ESSearchResponse); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 0, + page: 1, + perPage: 100, + }); + + // Act + defineGetCspStatusRoute(router); + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + await handler(mockContext, mockRequest, mockResponse); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]?.body; + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: 'indexed', + latestPackageVersion: '0.0.14', + installedPackagePolicies: 0, + healthyAgents: 0, + installedPackageVersion: undefined, + isPluginInitialized: false, + }); + }); + + it('Verify the API result when there are findings, installed policies, no running agents', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ + hits: { + hits: [{ Findings: 'foo' }], + }, + } as unknown as ESSearchResponse); + + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 3, + page: 1, + perPage: 100, + }); + + // Act + defineGetCspStatusRoute(router); + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + await handler(mockContext, mockRequest, mockResponse); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]?.body; + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: 'indexed', + latestPackageVersion: '0.0.14', + installedPackagePolicies: 3, + healthyAgents: 0, + installedPackageVersion: '0.0.14', + isPluginInitialized: false, + }); + }); + + it('Verify the API result when there are findings, installed policies, running agents', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ + hits: { + hits: [{ Findings: 'foo' }], + }, + } as unknown as ESSearchResponse); + + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 3, + page: 1, + perPage: 100, + }); + + mockAgentPolicyService.getByIds.mockResolvedValue([ + { package_policies: createPackagePolicyMock() }, + ] as unknown as AgentPolicy[]); + + mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ + online: 1, + updating: 0, + } as unknown as GetAgentStatusResponse['results']); + + // Act + defineGetCspStatusRoute(router); + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + await handler(mockContext, mockRequest, mockResponse); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]!.body; + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: 'indexed', + latestPackageVersion: '0.0.14', + installedPackagePolicies: 3, + healthyAgents: 1, + installedPackageVersion: '0.0.14', + isPluginInitialized: false, + }); + }); + + it('Verify the API result when there are no findings and no installed policies', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ + hits: { + hits: [], + }, + } as unknown as ESSearchResponse); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 0, + page: 1, + perPage: 100, + }); + defineGetCspStatusRoute(router); + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + + // Act + await handler(mockContext, mockRequest, mockResponse); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]!.body; + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: 'not-installed', + latestPackageVersion: '0.0.14', + installedPackagePolicies: 0, + healthyAgents: 0, + isPluginInitialized: false, + }); + }); + + it('Verify the API result when there are no findings, installed agent but no deployed agent', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ + hits: { + hits: [], + }, + } as unknown as ESSearchResponse); + + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 1, + page: 1, + perPage: 100, + }); + + mockAgentPolicyService.getByIds.mockResolvedValue([ + { package_policies: createPackagePolicyMock() }, + ] as unknown as AgentPolicy[]); + + mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ + online: 0, + updating: 0, + } as unknown as GetAgentStatusResponse['results']); + + // Act + defineGetCspStatusRoute(router); + + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + await handler(mockContext, mockRequest, mockResponse); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]!.body; + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: 'not-deployed', + latestPackageVersion: '0.0.14', + installedPackagePolicies: 1, + healthyAgents: 0, + installedPackageVersion: '0.0.14', + isPluginInitialized: false, + }); + }); + + it('Verify the API result when there are no findings, installed agent, deployed agent, before index timeout', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ + hits: { + hits: [], + }, + } as unknown as ESSearchResponse); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + + const currentTime = new Date(); + mockCspPackageInfo.install_started_at = new Date( + currentTime.setMinutes(currentTime.getMinutes() - INDEX_TIMEOUT_IN_MINUTES + 1) + ).toUTCString(); + + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 1, + page: 1, + perPage: 100, + }); + + mockAgentPolicyService.getByIds.mockResolvedValue([ + { package_policies: createPackagePolicyMock() }, + ] as unknown as AgentPolicy[]); + + mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ + online: 1, + updating: 0, + } as unknown as GetAgentStatusResponse['results']); + + // Act + defineGetCspStatusRoute(router); + + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]!.body; + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: 'indexing', + latestPackageVersion: '0.0.14', + installedPackagePolicies: 1, + healthyAgents: 1, + installedPackageVersion: '0.0.14', + isPluginInitialized: false, + }); + }); + + it('Verify the API result when there are no findings, installed agent, deployed agent, after index timeout', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ + hits: { + hits: [], + }, + } as unknown as ESSearchResponse); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + + const currentTime = new Date(); + mockCspPackageInfo.install_started_at = new Date( + currentTime.setMinutes(currentTime.getMinutes() - INDEX_TIMEOUT_IN_MINUTES - 1) + ).toUTCString(); + + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + + mockPackagePolicyService.list.mockResolvedValueOnce({ + items: [], + total: 1, + page: 1, + perPage: 100, + }); + + mockAgentPolicyService.getByIds.mockResolvedValue([ + { package_policies: createPackagePolicyMock() }, + ] as unknown as AgentPolicy[]); + + mockAgentClient.getAgentStatusForAgentPolicy.mockResolvedValue({ + online: 1, + updating: 0, + } as unknown as GetAgentStatusResponse['results']); + + // Act + defineGetCspStatusRoute(router); + + const [_, handler] = router.get.mock.calls[0]; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + + await handler(mockContext, mockRequest, mockResponse); + + // Assert + const [call] = mockResponse.ok.mock.calls; + const body = call[0]!.body; + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + + await expect(body).toMatchObject({ + status: 'index-timeout', + latestPackageVersion: '0.0.14', + installedPackagePolicies: 1, + healthyAgents: 1, + installedPackageVersion: '0.0.14', + isPluginInitialized: false, + }); + }); +}); diff --git a/x-pack/plugins/cloud_defend/server/routes/status/status.ts b/x-pack/plugins/cloud_defend/server/routes/status/status.ts new file mode 100644 index 0000000000000..438d9176cea31 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/routes/status/status.ts @@ -0,0 +1,213 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import type { SavedObjectsClientContract, Logger } from '@kbn/core/server'; +import type { AgentPolicyServiceInterface, AgentService } from '@kbn/fleet-plugin/server'; +import moment from 'moment'; +import { PackagePolicy } from '@kbn/fleet-plugin/common'; +import { schema } from '@kbn/config-schema'; +import { + ALERTS_INDEX_PATTERN, + INTEGRATION_PACKAGE_NAME, + STATUS_ROUTE_PATH, +} from '../../../common/constants'; +import type { CloudDefendApiRequestHandlerContext, CloudDefendRouter } from '../../types'; +import type { + CloudDefendSetupStatus, + CloudDefendStatusCode, + IndexStatus, +} from '../../../common/types'; +import { + getAgentStatusesByAgentPolicies, + getCloudDefendAgentPolicies, + getCloudDefendPackagePolicies, + getInstalledPolicyTemplates, +} from '../../lib/fleet_util'; +import { checkIndexStatus } from '../../lib/check_index_status'; + +export const INDEX_TIMEOUT_IN_MINUTES = 10; + +const calculateDiffFromNowInMinutes = (date: string | number): number => + moment().diff(moment(date), 'minutes'); + +const getHealthyAgents = async ( + soClient: SavedObjectsClientContract, + installedCloudDefendPackagePolicies: PackagePolicy[], + agentPolicyService: AgentPolicyServiceInterface, + agentService: AgentService, + logger: Logger +): Promise => { + // Get agent policies of package policies (from installed package policies) + const agentPolicies = await getCloudDefendAgentPolicies( + soClient, + installedCloudDefendPackagePolicies, + agentPolicyService + ); + + // Get agents statuses of the following agent policies + const agentStatusesByAgentPolicyId = await getAgentStatusesByAgentPolicies( + agentService, + agentPolicies, + logger + ); + + return Object.values(agentStatusesByAgentPolicyId).reduce( + (sum, status) => sum + status.online + status.updating, + 0 + ); +}; + +const calculateCloudDefendStatusCode = ( + indicesStatus: { + alerts: IndexStatus; + }, + installedCloudDefendPackagePolicies: number, + healthyAgents: number, + timeSinceInstallationInMinutes: number +): CloudDefendStatusCode => { + // We check privileges only for the relevant indices for our pages to appear + if (indicesStatus.alerts === 'unprivileged') return 'unprivileged'; + if (installedCloudDefendPackagePolicies === 0) return 'not-installed'; + if (healthyAgents === 0) return 'not-deployed'; + if (timeSinceInstallationInMinutes <= INDEX_TIMEOUT_IN_MINUTES) return 'indexing'; + if (timeSinceInstallationInMinutes > INDEX_TIMEOUT_IN_MINUTES) return 'index-timeout'; + + throw new Error('Could not determine csp status'); +}; + +const assertResponse = ( + resp: CloudDefendSetupStatus, + logger: CloudDefendApiRequestHandlerContext['logger'] +) => { + if ( + resp.status === 'unprivileged' && + !resp.indicesDetails.some((idxDetails) => idxDetails.status === 'unprivileged') + ) { + logger.warn('Returned status in `unprivileged` but response is missing the unprivileged index'); + } +}; + +const getCloudDefendStatus = async ({ + logger, + esClient, + soClient, + packageService, + packagePolicyService, + agentPolicyService, + agentService, +}: CloudDefendApiRequestHandlerContext): Promise => { + const [ + alertsIndexStatus, + installation, + latestCloudDefendPackage, + installedPackagePolicies, + installedPolicyTemplates, + ] = await Promise.all([ + checkIndexStatus(esClient.asCurrentUser, ALERTS_INDEX_PATTERN, logger), + packageService.asInternalUser.getInstallation(INTEGRATION_PACKAGE_NAME), + packageService.asInternalUser.fetchFindLatestPackage(INTEGRATION_PACKAGE_NAME), + getCloudDefendPackagePolicies(soClient, packagePolicyService, INTEGRATION_PACKAGE_NAME, { + per_page: 10000, + }), + getInstalledPolicyTemplates(packagePolicyService, soClient), + ]); + + const healthyAgents = await getHealthyAgents( + soClient, + installedPackagePolicies.items, + agentPolicyService, + agentService, + logger + ); + + const installedPackagePoliciesTotal = installedPackagePolicies.total; + const latestCloudDefendPackageVersion = latestCloudDefendPackage.version; + + const MIN_DATE = 0; + const indicesDetails = [ + { + index: ALERTS_INDEX_PATTERN, + status: alertsIndexStatus, + }, + ]; + + const status = calculateCloudDefendStatusCode( + { + alerts: alertsIndexStatus, + }, + installedPackagePoliciesTotal, + healthyAgents, + calculateDiffFromNowInMinutes(installation?.install_started_at || MIN_DATE) + ); + + if (status === 'not-installed') + return { + status, + indicesDetails, + latestPackageVersion: latestCloudDefendPackageVersion, + healthyAgents, + installedPackagePolicies: installedPackagePoliciesTotal, + }; + + const response = { + status, + indicesDetails, + latestPackageVersion: latestCloudDefendPackageVersion, + healthyAgents, + installedPolicyTemplates, + installedPackagePolicies: installedPackagePoliciesTotal, + installedPackageVersion: installation?.install_version, + }; + + assertResponse(response, logger); + return response; +}; + +export const statusQueryParamsSchema = schema.object({ + /** + * CSP Plugin initialization includes creating indices/transforms/tasks. + * Prior to this initialization, the plugin is not ready to index findings. + */ + check: schema.oneOf([schema.literal('all'), schema.literal('init')], { defaultValue: 'all' }), +}); + +export const defineGetCloudDefendStatusRoute = (router: CloudDefendRouter): void => + router.get( + { + path: STATUS_ROUTE_PATH, + validate: { query: statusQueryParamsSchema }, + options: { + tags: ['access:cloud-defend-read'], + }, + }, + async (context, request, response) => { + const cloudDefendContext = await context.cloudDefend; + try { + /* if (request.query.check === 'init') { + return response.ok({ + body: { + isPluginInitialized: cloudDefendContext.isPluginInitialized(), + }, + }); + }*/ + const status = await getCloudDefendStatus(cloudDefendContext); + return response.ok({ + body: status, + }); + } catch (err) { + cloudDefendContext.logger.error(`Error getting cloud_defend status`); + cloudDefendContext.logger.error(err); + + const error = transformError(err); + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode, + }); + } + } + ); diff --git a/x-pack/plugins/cloud_defend/server/types.ts b/x-pack/plugins/cloud_defend/server/types.ts index f68cf28e317c0..b624c1793ee0a 100644 --- a/x-pack/plugins/cloud_defend/server/types.ts +++ b/x-pack/plugins/cloud_defend/server/types.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { CloudSetup } from '@kbn/cloud-plugin/server'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; import type { IRouter, @@ -33,6 +34,7 @@ export interface CloudDefendPluginStart {} export interface CloudDefendPluginSetupDeps { data: DataPluginSetup; security: SecurityPluginSetup; + cloud: CloudSetup; } export interface CloudDefendPluginStartDeps { data: DataPluginStart; @@ -49,7 +51,6 @@ export interface CloudDefendApiRequestHandlerContext { agentService: AgentService; packagePolicyService: PackagePolicyClient; packageService: PackageService; - isPluginInitialized(): boolean; } export type CloudDefendRequestHandlerContext = CustomRequestHandlerContext<{ diff --git a/x-pack/plugins/cloud_defend/tsconfig.json b/x-pack/plugins/cloud_defend/tsconfig.json index 741a954da0331..7326b5cfa8cb4 100755 --- a/x-pack/plugins/cloud_defend/tsconfig.json +++ b/x-pack/plugins/cloud_defend/tsconfig.json @@ -6,13 +6,19 @@ "include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*"], "kbn_references": [ "@kbn/core", + "@kbn/data-plugin", + "@kbn/security-plugin", "@kbn/fleet-plugin", "@kbn/i18n-react", + "@kbn/config-schema", + "@kbn/licensing-plugin", "@kbn/data-plugin", "@kbn/kibana-react-plugin", "@kbn/monaco", "@kbn/i18n", - "@kbn/usage-collection-plugin" + "@kbn/usage-collection-plugin", + "@kbn/cloud-plugin", + "@kbn/unified-search-plugin" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/security_solution/kibana.jsonc b/x-pack/plugins/security_solution/kibana.jsonc index 6f6c371f5f27f..85418bdeb31b7 100644 --- a/x-pack/plugins/security_solution/kibana.jsonc +++ b/x-pack/plugins/security_solution/kibana.jsonc @@ -15,6 +15,7 @@ "alerting", "cases", "cloud", + "cloudDefend", "cloudSecurityPosture", "dashboard", "data", diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index b4dcc3a4a2b7b..87d43742a9433 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -533,7 +533,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ ...getCloudPostureSecuritySolutionLink('benchmarks'), }, { - ...getCloudDefendSecuritySolutionLink('benchmarks'), + ...getCloudDefendSecuritySolutionLink('policies'), }, ], }, diff --git a/x-pack/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/plugins/security_solution/public/cloud_defend/links.ts index 0a92946cf929d..b34f518bb8b4f 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_defend/links.ts @@ -15,6 +15,12 @@ const commonLinkProperties: Partial = { capabilities: [`${SERVER_APP_ID}.show`], }; +export const rootLinks: LinkItem = { + ...getSecuritySolutionLink('policies'), + globalNavPosition: 3, + ...commonLinkProperties, +}; + export const manageLinks: LinkItem = { ...getSecuritySolutionLink('policies'), description: i18n.translate('xpack.securitySolution.appLinks.cloudDefendPoliciesDescription', { @@ -24,8 +30,6 @@ export const manageLinks: LinkItem = { ...commonLinkProperties, }; -console.log(manageLinks); - export const manageCategories: LinkCategories = [ { label: i18n.translate('xpack.securitySolution.appLinks.category.cloudDefend', { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 20d09e38d32b6..8efd6b8d2f5bb 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -14,6 +14,7 @@ import { getTrailingBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../.. import { getTrailingBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../explore/network/pages/details'; import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; import { getTrailingBreadcrumbs as geExceptionsBreadcrumbs } from '../../../../exceptions/utils/pages.utils'; +import { getTrailingBreadcrumbs as getCloudDefendBreadcrumbs } from '../../../../cloud_defend/breadcrumbs'; import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_security_posture/breadcrumbs'; import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../explore/users/pages/details/utils'; import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs'; @@ -131,6 +132,8 @@ const getTrailingBreadcrumbsForRoutes = ( return getAlertDetailBreadcrumbs(spyState, getSecuritySolutionUrl); case SecurityPageName.cloudSecurityPostureBenchmarks: return getCSPBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.cloudDefendPolicies: + return getCloudDefendBreadcrumbs(spyState, getSecuritySolutionUrl); } return []; diff --git a/x-pack/plugins/security_solution/public/common/links/app_links.ts b/x-pack/plugins/security_solution/public/common/links/app_links.ts index c00fa87f878da..719dc819c812c 100644 --- a/x-pack/plugins/security_solution/public/common/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/common/links/app_links.ts @@ -14,6 +14,7 @@ import { links as managementLinks, getManagementFilteredLinks } from '../../mana import { dashboardsLandingLinks, threatHuntingLandingLinks } from '../../landing_pages/links'; import { gettingStartedLinks } from '../../overview/links'; import { rootLinks as cloudSecurityPostureRootLinks } from '../../cloud_security_posture/links'; +import { rootLinks as cloudDefendRootLinks } from '../../cloud_defend/links'; import type { StartPlugins } from '../../types'; const casesLinks = getCasesLinkItems(); @@ -21,6 +22,7 @@ const casesLinks = getCasesLinkItems(); export const links = Object.freeze([ dashboardsLandingLinks, detectionLinks, + cloudDefendRootLinks, cloudSecurityPostureRootLinks, timelinesLinks, casesLinks, diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index 1d93699ff1b47..66aefc6db3e08 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -20,6 +20,7 @@ import { Rules } from './rules'; import { Timelines } from './timelines'; import { Management } from './management'; import { LandingPages } from './landing_pages'; +import { CloudDefend } from './cloud_defend'; import { CloudSecurityPosture } from './cloud_security_posture'; import { ThreatIntelligence } from './threat_intelligence'; @@ -37,6 +38,7 @@ const subPluginClasses = { Timelines, Management, LandingPages, + CloudDefend, CloudSecurityPosture, ThreatIntelligence, }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index abc9c41101e25..78680086d45c0 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -395,6 +395,7 @@ export class Plugin implements IPlugin Date: Thu, 16 Feb 2023 00:47:24 +0000 Subject: [PATCH 04/19] manage link moved into same category as csp (looks cleaner). logo and copy updated --- .../public/common/navigation/constants.ts | 2 +- .../public/common/navigation/query_utils.ts | 40 ----------------- .../public/pages/policies/index.tsx | 6 +-- .../public/cloud_defend/breadcrumbs.ts | 25 ----------- .../public/cloud_defend/links.ts | 17 +++++--- .../public/cloud_security_posture/links.ts | 5 ++- .../navigation/breadcrumbs/index.ts | 3 -- .../public/management/icons/cloud_defend.tsx | 43 +++++++++++++++++++ .../public/management/links.ts | 3 +- 9 files changed, 62 insertions(+), 82 deletions(-) delete mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts delete mode 100644 x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts create mode 100644 x-pack/plugins/security_solution/public/management/icons/cloud_defend.tsx diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts b/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts index e445398f69087..b166184dddb87 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/constants.ts @@ -10,7 +10,7 @@ import type { CloudDefendPage, CloudDefendPageNavigationItem } from './types'; const NAV_ITEMS_NAMES = { POLICIES: i18n.translate('xpack.cloudDefend.navigation.policiesNavItemLabel', { - defaultMessage: 'Policies', + defaultMessage: 'Defend for containers (D4C)', }), }; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts b/x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts deleted file mode 100644 index 3a051456733a6..0000000000000 --- a/x-pack/plugins/cloud_defend/public/common/navigation/query_utils.ts +++ /dev/null @@ -1,40 +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 { encode, decode } from '@kbn/rison'; -import type { LocationDescriptorObject } from 'history'; - -const encodeRison = (v: any): string | undefined => { - try { - return encode(v); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } -}; - -const decodeRison = (query: string): T | undefined => { - try { - return decode(query) as T; - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } -}; - -const QUERY_PARAM_KEY = 'cspq'; - -export const encodeQuery = (query: any): LocationDescriptorObject['search'] => { - const risonQuery = encodeRison(query); - if (!risonQuery) return; - return `${QUERY_PARAM_KEY}=${risonQuery}`; -}; - -export const decodeQuery = (search?: string): Partial | undefined => { - const risonQuery = new URLSearchParams(search).get(QUERY_PARAM_KEY); - if (!risonQuery) return; - return decodeRison(risonQuery); -}; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx index 4aa2e29541b79..ace1720b50a57 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx @@ -58,7 +58,7 @@ const EmptyState = ({ name }: { name: string }) => ( {name && ( ( @@ -144,7 +144,7 @@ export const Policies = () => { pageTitle={ } diff --git a/x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts b/x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts deleted file mode 100644 index 16bd30db6680d..0000000000000 --- a/x-pack/plugins/security_solution/public/cloud_defend/breadcrumbs.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; -import type { GetSecuritySolutionUrl } from '../common/components/link_to'; -import type { RouteSpyState } from '../common/utils/route/types'; - -export const getTrailingBreadcrumbs = ( - params: RouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { - const breadcrumbs = []; - - if (params.state?.ruleName) { - breadcrumbs.push({ - text: params.state.ruleName, - }); - } - - return breadcrumbs; -}; diff --git a/x-pack/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/plugins/security_solution/public/cloud_defend/links.ts index b34f518bb8b4f..80b1ba83c2033 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_defend/links.ts @@ -6,9 +6,10 @@ */ import { getSecuritySolutionLink } from '@kbn/cloud-defend-plugin/public'; import { i18n } from '@kbn/i18n'; -import { SecurityPageName, SERVER_APP_ID } from '../../common/constants'; -import type { LinkCategories, LinkItem } from '../common/links/types'; -import { IconExceptionLists } from '../management/icons/exception_lists'; +import type { SecurityPageName } from '../../common/constants'; +import { SERVER_APP_ID } from '../../common/constants'; +import type { LinkItem } from '../common/links/types'; +import { IconCloudDefend } from '../management/icons/cloud_defend'; const commonLinkProperties: Partial = { hideTimeline: true, @@ -24,17 +25,19 @@ export const rootLinks: LinkItem = { export const manageLinks: LinkItem = { ...getSecuritySolutionLink('policies'), description: i18n.translate('xpack.securitySolution.appLinks.cloudDefendPoliciesDescription', { - defaultMessage: 'View control policies.', + defaultMessage: 'View drift prevention policies.', }), - landingIcon: IconExceptionLists, + landingIcon: IconCloudDefend, ...commonLinkProperties, }; -export const manageCategories: LinkCategories = [ +// currently using the CSP category, as it's weird to have two categories each with one item.. +// saving this for when we add other pages +/* export const manageCategories: LinkCategories = [ { label: i18n.translate('xpack.securitySolution.appLinks.category.cloudDefend', { defaultMessage: 'DEFEND FOR CONTAINERS (D4C)', }), linkIds: [SecurityPageName.cloudDefendPolicies], }, -]; +]; */ diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts b/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts index def3b0ed9f5eb..e4c0dfd1f20db 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/links.ts @@ -51,6 +51,9 @@ export const manageCategories: LinkCategories = [ label: i18n.translate('xpack.securitySolution.appLinks.category.cloudSecurityPosture', { defaultMessage: 'CLOUD SECURITY POSTURE', }), - linkIds: [SecurityPageName.cloudSecurityPostureBenchmarks], + linkIds: [ + SecurityPageName.cloudSecurityPostureBenchmarks, + SecurityPageName.cloudDefendPolicies, + ], }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 8efd6b8d2f5bb..20d09e38d32b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -14,7 +14,6 @@ import { getTrailingBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../.. import { getTrailingBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../explore/network/pages/details'; import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; import { getTrailingBreadcrumbs as geExceptionsBreadcrumbs } from '../../../../exceptions/utils/pages.utils'; -import { getTrailingBreadcrumbs as getCloudDefendBreadcrumbs } from '../../../../cloud_defend/breadcrumbs'; import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_security_posture/breadcrumbs'; import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../explore/users/pages/details/utils'; import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs'; @@ -132,8 +131,6 @@ const getTrailingBreadcrumbsForRoutes = ( return getAlertDetailBreadcrumbs(spyState, getSecuritySolutionUrl); case SecurityPageName.cloudSecurityPostureBenchmarks: return getCSPBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.cloudDefendPolicies: - return getCloudDefendBreadcrumbs(spyState, getSecuritySolutionUrl); } return []; diff --git a/x-pack/plugins/security_solution/public/management/icons/cloud_defend.tsx b/x-pack/plugins/security_solution/public/management/icons/cloud_defend.tsx new file mode 100644 index 0000000000000..1eb5a656b3628 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/icons/cloud_defend.tsx @@ -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 type { SVGProps } from 'react'; +import React from 'react'; +export const IconCloudDefend: React.FC> = ({ ...props }) => ( + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 911b0e0644824..0d644922da21e 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -50,7 +50,7 @@ import { manageLinks as cloudSecurityPostureLinks, } from '../cloud_security_posture/links'; import { - manageCategories as cloudDefendCategories, + // manageCategories as cloudDefendCategories, manageLinks as cloudDefendLinks, } from '../cloud_defend/links'; import { IconActionHistory } from './icons/action_history'; @@ -87,7 +87,6 @@ const categories = [ ], }, ...cloudSecurityPostureCategories, - ...cloudDefendCategories, ]; export const links: LinkItem = { From 2d83259a36a8bea64d81d3b68c7b3a9654a02fa5 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Thu, 16 Feb 2023 22:18:17 +0000 Subject: [PATCH 05/19] frontend unit tests passing --- .../cloud_defend/public/application/route.tsx | 3 +- .../public/application/router.test.tsx | 11 +-- .../public/application/router.tsx | 3 +- .../security_solution_links.test.ts | 26 +++---- .../use_cloud_defend_integration_link.ts | 50 ------------ .../cloud_defend_page/index.test.tsx | 77 ++++++++++--------- .../components/cloud_defend_page/index.tsx | 2 +- .../components/policies_table/index.test.tsx | 54 ++++++------- .../components/policies_table/index.tsx | 1 - .../public/pages/policies/index.test.tsx | 55 ++++++------- .../public/pages/policies/index.tsx | 2 +- .../public/pages/policies/test_subjects.ts | 3 - x-pack/plugins/cloud_defend/public/plugin.tsx | 21 ++--- 13 files changed, 128 insertions(+), 180 deletions(-) delete mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts diff --git a/x-pack/plugins/cloud_defend/public/application/route.tsx b/x-pack/plugins/cloud_defend/public/application/route.tsx index 5b35330bb961c..27959ec0845e5 100644 --- a/x-pack/plugins/cloud_defend/public/application/route.tsx +++ b/x-pack/plugins/cloud_defend/public/application/route.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { Route, type RouteProps } from 'react-router-dom'; +import { Route } from '@kbn/shared-ux-router'; +import { type RouteProps } from 'react-router-dom'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { cloudDefendPages } from '../common/navigation/constants'; import { useSecuritySolutionContext } from './security_solution_context'; diff --git a/x-pack/plugins/cloud_defend/public/application/router.test.tsx b/x-pack/plugins/cloud_defend/public/application/router.test.tsx index a48a4943b3a5b..af7766a290fca 100644 --- a/x-pack/plugins/cloud_defend/public/application/router.test.tsx +++ b/x-pack/plugins/cloud_defend/public/application/router.test.tsx @@ -14,8 +14,8 @@ import { createMemoryHistory, MemoryHistory } from 'history'; import * as constants from '../common/navigation/constants'; import { QueryClientProviderProps } from '@tanstack/react-query'; -jest.mock('../pages', () => ({ - Policies: () =>

policies
, +jest.mock('../pages/policies', () => ({ + Policies: () =>
Policies
, })); jest.mock('@tanstack/react-query', () => ({ @@ -90,12 +90,5 @@ describe('CloudDefendRouter', () => { expect(result.queryByTestId('mockedSpyRoute')).toBeInTheDocument(); }); - - it('should not render SpyRoute for dynamic paths', () => { - history.push('/cloud_defend/policies/packagePolicyId/policyId/rules'); - const result = renderCloudDefendRouter(); - - expect(result.queryByTestId('mockedSpyRoute')).not.toBeInTheDocument(); - }); }); }); diff --git a/x-pack/plugins/cloud_defend/public/application/router.tsx b/x-pack/plugins/cloud_defend/public/application/router.tsx index 46ff3e58c3167..6ffd159c2d09a 100644 --- a/x-pack/plugins/cloud_defend/public/application/router.tsx +++ b/x-pack/plugins/cloud_defend/public/application/router.tsx @@ -7,7 +7,8 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { Redirect, Switch } from 'react-router-dom'; +import { Route } from '@kbn/shared-ux-router'; import { cloudDefendPages } from '../common/navigation/constants'; import type { CloudDefendSecuritySolutionContext } from '../types'; import { SecuritySolutionContext } from './security_solution_context'; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts index 072776cde5a0c..f6cfe42d0583a 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.test.ts @@ -5,35 +5,35 @@ * 2.0. */ -import { cloudPosturePages } from './constants'; +import { cloudDefendPages } from './constants'; import { getSecuritySolutionLink, getSecuritySolutionNavTab } from './security_solution_links'; import { Chance } from 'chance'; -import type { CspPage } from './types'; +import type { CloudDefendPage } from './types'; const chance = new Chance(); describe('getSecuritySolutionLink', () => { it('gets the correct link properties', () => { - const cspPage = chance.pickone(['dashboard', 'findings', 'benchmarks']); + const cloudDefendPage = chance.pickone(['policies']); - const link = getSecuritySolutionLink(cspPage); + const link = getSecuritySolutionLink(cloudDefendPage); - expect(link.id).toEqual(cloudPosturePages[cspPage].id); - expect(link.path).toEqual(cloudPosturePages[cspPage].path); - expect(link.title).toEqual(cloudPosturePages[cspPage].name); + expect(link.id).toEqual(cloudDefendPages[cloudDefendPage].id); + expect(link.path).toEqual(cloudDefendPages[cloudDefendPage].path); + expect(link.title).toEqual(cloudDefendPages[cloudDefendPage].name); }); }); describe('getSecuritySolutionNavTab', () => { it('gets the correct nav tab properties', () => { - const cspPage = chance.pickone(['dashboard', 'findings', 'benchmarks']); + const cloudDefendPage = chance.pickone(['policies']); const basePath = chance.word(); - const navTab = getSecuritySolutionNavTab(cspPage, basePath); + const navTab = getSecuritySolutionNavTab(cloudDefendPage, basePath); - expect(navTab.id).toEqual(cloudPosturePages[cspPage].id); - expect(navTab.name).toEqual(cloudPosturePages[cspPage].name); - expect(navTab.href).toEqual(`${basePath}${cloudPosturePages[cspPage].path}`); - expect(navTab.disabled).toEqual(!!cloudPosturePages[cspPage].disabled); + expect(navTab.id).toEqual(cloudDefendPages[cloudDefendPage].id); + expect(navTab.name).toEqual(cloudDefendPages[cloudDefendPage].name); + expect(navTab.href).toEqual(`${basePath}${cloudDefendPages[cloudDefendPage].path}`); + expect(navTab.disabled).toEqual(!!cloudDefendPages[cloudDefendPage].disabled); }); }); diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts b/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts deleted file mode 100644 index b2d0cca31fcba..0000000000000 --- a/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_link.ts +++ /dev/null @@ -1,50 +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 { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public'; -import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; -import { useCloudDefendIntegration } from '../api/use_cloud_defend_integration'; -import { useKibana } from '../hooks/use_kibana'; - -export const useCloudDefendIntegrationLinks = (): { - addIntegrationLink: string | undefined; - docsLink: string; -} => { - const { http } = useKibana().services; - const cloudDefendIntegration = useCloudDefendIntegration(); - - if (!cloudDefendIntegration.isSuccess) - return { - addIntegrationLink: undefined, - docsLink: 'https://www.elastic.co/guide/index.html', - }; - - const addIntegrationLink = pagePathGetters - .add_integration_to_policy({ - integration: INTEGRATION_PACKAGE_NAME, - pkgkey: pkgKeyFromPackageInfo({ - name: cloudDefendIntegration.data.item.name, - version: cloudDefendIntegration.data.item.version, - }), - }) - .join(''); - - const docsLink = pagePathGetters - .integration_details_overview({ - integration: INTEGRATION_PACKAGE_NAME, - pkgkey: pkgKeyFromPackageInfo({ - name: cloudDefendIntegration.data.item.name, - version: cloudDefendIntegration.data.item.version, - }), - }) - .join(''); - - return { - addIntegrationLink: http.basePath.prepend(addIntegrationLink), - docsLink: http.basePath.prepend(docsLink), - }; -}; diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx index 749aa1ccb038a..6394e805a07c8 100644 --- a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx @@ -5,53 +5,58 @@ * 2.0. */ -import { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; +// import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; import Chance from 'chance'; import { + CloudDefendPage, DEFAULT_NO_DATA_TEST_SUBJECT, ERROR_STATE_TEST_SUBJECT, isCommonError, LOADING_STATE_TEST_SUBJECT, PACKAGE_NOT_INSTALLED_TEST_SUBJECT, SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT, -} from './cloud_posture_page'; -import { createReactQueryResponse } from '../test/fixtures/react_query'; -import { TestProvider } from '../test/test_provider'; +} from '.'; +import { createReactQueryResponse } from '../../test/fixtures/react_query'; +import { TestProvider } from '../../test/test_provider'; import { coreMock } from '@kbn/core/public/mocks'; import { render, screen } from '@testing-library/react'; import React, { ComponentProps } from 'react'; 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 { useCspIntegrationLink } from '../common/navigation/use_csp_integration_link'; +import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api'; +import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links'; const chance = new Chance(); -jest.mock('../common/api/use_setup_status_api'); -jest.mock('../common/hooks/use_subscription_status'); -jest.mock('../common/navigation/use_csp_integration_link'); +jest.mock('../../common/api/use_setup_status_api'); +// jest.mock('../../common/hooks/use_subscription_status'); +jest.mock('../../common/navigation/use_cloud_defend_integration_links'); -describe('', () => { +describe('', () => { beforeEach(() => { jest.resetAllMocks(); - (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + (useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', data: { status: 'indexed' }, }) ); - (useSubscriptionStatus as jest.Mock).mockImplementation(() => + (useCloudDefendIntegrationLinks as jest.Mock).mockImplementation(() => ({ + addIntegrationLink: chance.url(), + docsLink: chance.url(), + })); + + /* (useSubscriptionStatus as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', data: true, }) - ); + );*/ }); - const renderCloudPosturePage = ( - props: ComponentProps = { children: null } + const renderCloudDefendPage = ( + props: ComponentProps = { children: null } ) => { const mockCore = coreMock.createStart(); @@ -69,14 +74,14 @@ describe('', () => { }, }} > - + ); }; it('renders children if setup status is indexed', () => { const children = chance.sentence(); - renderCloudPosturePage({ children }); + renderCloudDefendPage({ children }); expect(screen.getByText(children)).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -85,7 +90,7 @@ describe('', () => { expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); }); - it('renders default loading state when the subscription query is loading', () => { + /* it('renders default loading state when the subscription query is loading', () => { (useSubscriptionStatus as jest.Mock).mockImplementation( () => createReactQueryResponse({ @@ -94,7 +99,7 @@ describe('', () => { ); const children = chance.sentence(); - renderCloudPosturePage({ children }); + renderCloudDefendPage({ children }); expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); @@ -113,7 +118,7 @@ describe('', () => { ); const children = chance.sentence(); - renderCloudPosturePage({ children }); + renderCloudDefendPage({ children }); expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -131,7 +136,7 @@ describe('', () => { ); const children = chance.sentence(); - renderCloudPosturePage({ children }); + renderCloudDefendPage({ children }); expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); @@ -139,18 +144,18 @@ describe('', () => { expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); }); + */ it('renders integrations installation prompt if integration is not installed', () => { - (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + (useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', data: { status: 'not-installed' }, }) ); - (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); const children = chance.sentence(); - renderCloudPosturePage({ children }); + renderCloudDefendPage({ children }); expect(screen.getByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); @@ -160,7 +165,7 @@ describe('', () => { }); it('renders default loading state when the integration query is loading', () => { - (useCspSetupStatusApi as jest.Mock).mockImplementation( + (useCloudDefendSetupStatusApi as jest.Mock).mockImplementation( () => createReactQueryResponse({ status: 'loading', @@ -168,7 +173,7 @@ describe('', () => { ); const children = chance.sentence(); - renderCloudPosturePage({ children }); + renderCloudDefendPage({ children }); expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByText(children)).not.toBeInTheDocument(); @@ -178,7 +183,7 @@ describe('', () => { }); it('renders default error state when the integration query has an error', () => { - (useCspSetupStatusApi as jest.Mock).mockImplementation( + (useCloudDefendSetupStatusApi as jest.Mock).mockImplementation( () => createReactQueryResponse({ status: 'error', @@ -187,7 +192,7 @@ describe('', () => { ); const children = chance.sentence(); - renderCloudPosturePage({ children }); + renderCloudDefendPage({ children }); expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -202,7 +207,7 @@ describe('', () => { }) as unknown as UseQueryResult; const children = chance.sentence(); - renderCloudPosturePage({ children, query }); + renderCloudDefendPage({ children, query }); expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -217,7 +222,7 @@ describe('', () => { }) as unknown as UseQueryResult; const children = chance.sentence(); - renderCloudPosturePage({ children, query }); + renderCloudDefendPage({ children, query }); expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -243,7 +248,7 @@ describe('', () => { }) as unknown as UseQueryResult; const children = chance.sentence(); - renderCloudPosturePage({ children, query }); + renderCloudDefendPage({ children, query }); [error, message, statusCode].forEach((text) => expect(screen.getByText(text, { exact: false })).toBeInTheDocument() @@ -272,7 +277,7 @@ describe('', () => { }) as unknown as UseQueryResult; const children = chance.sentence(); - renderCloudPosturePage({ + renderCloudDefendPage({ children, query, errorRender: (err) =>
{isCommonError(err) && err.body.message}
, @@ -295,7 +300,7 @@ describe('', () => { }) as unknown as UseQueryResult; const children = chance.sentence(); - renderCloudPosturePage({ + renderCloudDefendPage({ children, query, loadingRender: () =>
{loading}
, @@ -316,7 +321,7 @@ describe('', () => { }) as unknown as UseQueryResult; const children = chance.sentence(); - renderCloudPosturePage({ children, query }); + renderCloudDefendPage({ children, query }); expect(screen.getByTestId(DEFAULT_NO_DATA_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); @@ -340,7 +345,7 @@ describe('', () => { }) as unknown as UseQueryResult; const children = chance.sentence(); - renderCloudPosturePage({ + renderCloudDefendPage({ children, query, noDataRenderer, diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx index b8526bd2cfdb5..71b5a752d17ce 100644 --- a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx @@ -23,7 +23,7 @@ import { css } from '@emotion/react'; import { FullSizeCenteredPage } from '../full_size_page'; import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api'; import { LoadingState } from '../loading_state'; -import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_link'; +import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links'; import noDataIllustration from '../../assets/icons/logo.svg'; diff --git a/x-pack/plugins/cloud_defend/public/components/policies_table/index.test.tsx b/x-pack/plugins/cloud_defend/public/components/policies_table/index.test.tsx index dc980df0d67ae..e473fd9f990b1 100644 --- a/x-pack/plugins/cloud_defend/public/components/policies_table/index.test.tsx +++ b/x-pack/plugins/cloud_defend/public/components/policies_table/index.test.tsx @@ -8,11 +8,11 @@ import React from 'react'; import Chance from 'chance'; import { render, screen } from '@testing-library/react'; import moment from 'moment'; -import { createCspBenchmarkIntegrationFixture } from '../../test/fixtures/csp_benchmark_integration'; -import { BenchmarksTable } from './benchmarks_table'; +import { createCloudDefendIntegrationFixture } from '../../test/fixtures/cloud_defend_integration'; +import { PoliciesTable } from '.'; import { TestProvider } from '../../test/test_provider'; -describe('', () => { +describe('', () => { const chance = new Chance(); const tableProps = { @@ -24,16 +24,16 @@ describe('', () => { }; it('renders integration name', () => { - const item = createCspBenchmarkIntegrationFixture(); - const benchmarks = [item]; + const item = createCloudDefendIntegrationFixture(); + const policies = [item]; render( - @@ -49,15 +49,15 @@ describe('', () => { agents: chance.integer({ min: 1 }), }; - const benchmarks = [createCspBenchmarkIntegrationFixture({ agent_policy: agentPolicy })]; + const policies = [createCloudDefendIntegrationFixture({ agent_policy: agentPolicy })]; render( - @@ -67,16 +67,16 @@ describe('', () => { }); it('renders number of agents', () => { - const item = createCspBenchmarkIntegrationFixture(); - const benchmarks = [item]; + const item = createCloudDefendIntegrationFixture(); + const policies = [item]; render( - @@ -87,16 +87,16 @@ describe('', () => { }); it('renders created by', () => { - const item = createCspBenchmarkIntegrationFixture(); - const benchmarks = [item]; + const item = createCloudDefendIntegrationFixture(); + const policies = [item]; render( - @@ -106,16 +106,16 @@ describe('', () => { }); it('renders created at', () => { - const item = createCspBenchmarkIntegrationFixture(); - const benchmarks = [item]; + const item = createCloudDefendIntegrationFixture(); + const policies = [item]; render( - diff --git a/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx b/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx index 00f487cac8c17..bb4134b083d9f 100644 --- a/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx +++ b/x-pack/plugins/cloud_defend/public/components/policies_table/index.tsx @@ -14,7 +14,6 @@ import { EuiLink, } from '@elastic/eui'; import React from 'react'; -import { generatePath } from 'react-router-dom'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; import { i18n } from '@kbn/i18n'; import { TimestampTableCell } from '../timestamp_table_cell'; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx index 1d8b3d6e55a91..41f836f6540d1 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx @@ -8,84 +8,85 @@ import React from 'react'; import Chance from 'chance'; import { render, screen } from '@testing-library/react'; import type { UseQueryResult } from '@tanstack/react-query'; -import { createCspBenchmarkIntegrationFixture } from '../../test/fixtures/csp_benchmark_integration'; +import { createCloudDefendIntegrationFixture } from '../../test/fixtures/cloud_defend_integration'; import { createReactQueryResponse } from '../../test/fixtures/react_query'; import { TestProvider } from '../../test/test_provider'; -import { Benchmarks } from './benchmarks'; +import { Policies } from '.'; 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 { useCspIntegrationLink } from '../../common/navigation/use_csp_integration_link'; +import { useCloudDefendPolicies } from './use_cloud_defend_policies'; +import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api'; +// import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; +import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links'; -jest.mock('./use_csp_benchmark_integrations'); +jest.mock('./use_cloud_defend_policies'); jest.mock('../../common/api/use_setup_status_api'); -jest.mock('../../common/hooks/use_subscription_status'); -jest.mock('../../common/navigation/use_csp_integration_link'); +// jest.mock('../../common/hooks/use_subscription_status'); +jest.mock('../../common/navigation/use_cloud_defend_integration_links'); const chance = new Chance(); -describe('', () => { +describe('', () => { beforeEach(() => { jest.resetAllMocks(); - (useCspSetupStatusApi as jest.Mock).mockImplementation(() => + (useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', data: { status: 'indexed' }, }) ); - (useSubscriptionStatus as jest.Mock).mockImplementation(() => + /* (useSubscriptionStatus as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', data: true, }) - ); + ); */ - (useCspIntegrationLink as jest.Mock).mockImplementation(() => chance.url()); + (useCloudDefendIntegrationLinks as jest.Mock).mockImplementation(() => ({ + addIntegrationLink: chance.url(), + docsLink: chance.url(), + })); }); - const renderBenchmarks = ( - queryResponse: Partial = createReactQueryResponse() - ) => { - (useCspBenchmarkIntegrations as jest.Mock).mockImplementation(() => queryResponse); + const renderPolicies = (queryResponse: Partial = createReactQueryResponse()) => { + (useCloudDefendPolicies as jest.Mock).mockImplementation(() => queryResponse); return render( - + ); }; it('renders the page header', () => { - renderBenchmarks(); + renderPolicies(); - expect(screen.getByTestId(TEST_SUBJ.BENCHMARKS_PAGE_HEADER)).toBeInTheDocument(); + expect(screen.getByTestId(TEST_SUBJ.POLICIES_PAGE_HEADER)).toBeInTheDocument(); }); it('renders the "add integration" button', () => { - renderBenchmarks(); + renderPolicies(); expect(screen.getByTestId(TEST_SUBJ.ADD_INTEGRATION_TEST_SUBJ)).toBeInTheDocument(); }); it('renders error state while there is an error', () => { const error = new Error('message'); - renderBenchmarks(createReactQueryResponse({ status: 'error', error })); + renderPolicies(createReactQueryResponse({ status: 'error', error })); expect(screen.getByText(error.message)).toBeInTheDocument(); }); it('renders the benchmarks table', () => { - renderBenchmarks( + renderPolicies( createReactQueryResponse({ status: 'success', - data: { total: 1, items: [createCspBenchmarkIntegrationFixture()] }, + data: { total: 1, items: [createCloudDefendIntegrationFixture()] }, }) ); - expect(screen.getByTestId(TEST_SUBJ.BENCHMARKS_TABLE_DATA_TEST_SUBJ)).toBeInTheDocument(); - Object.values(TEST_SUBJ.BENCHMARKS_TABLE_COLUMNS).forEach((testId) => + expect(screen.getByTestId(TEST_SUBJ.POLICIES_TABLE_DATA_TEST_SUBJ)).toBeInTheDocument(); + Object.values(TEST_SUBJ.POLICIES_TABLE_COLUMNS).forEach((testId) => expect(screen.getAllByTestId(testId)[0]).toBeInTheDocument() ); }); diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx index ace1720b50a57..3c351c57ecdd2 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx @@ -28,7 +28,7 @@ import { extractErrorMessage } from '../../../common/utils/helpers'; import * as TEST_SUBJ from './test_subjects'; import { LOCAL_STORAGE_PAGE_SIZE } from '../../common/constants'; import { usePageSize } from '../../common/hooks/use_page_size'; -import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_link'; +import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links'; const SEARCH_DEBOUNCE_MS = 300; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts b/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts index 91df8ace83b2c..9709360512727 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts +++ b/x-pack/plugins/cloud_defend/public/pages/policies/test_subjects.ts @@ -10,9 +10,6 @@ export const POLICIES_TABLE_DATA_TEST_SUBJ = 'cloud_defend_policies_table'; export const ADD_INTEGRATION_TEST_SUBJ = 'cloud_defend_add_integration'; export const POLICIES_TABLE_COLUMNS = { INTEGRATION_NAME: 'policies-table-column-integration-name', - MONITORING: 'policies-table-column-monitoring', - RULES: 'policies-table-column-rules', - INTEGRATION: 'policies-table-column-integration', AGENT_POLICY: 'policies-table-column-agent-policy', NUMBER_OF_AGENTS: 'policies-table-column-number-of-agents', CREATED_BY: 'policies-table-column-created-by', diff --git a/x-pack/plugins/cloud_defend/public/plugin.tsx b/x-pack/plugins/cloud_defend/public/plugin.tsx index aa9be3d218aa3..f52c9bef2a34e 100755 --- a/x-pack/plugins/cloud_defend/public/plugin.tsx +++ b/x-pack/plugins/cloud_defend/public/plugin.tsx @@ -48,17 +48,18 @@ export class CloudDefendPlugin implements Plugin ( + + +
+ +
+
+
+ ); + return { - getCloudDefendRouter: () => (props: CloudDefendRouterProps) => - ( - - -
- -
-
-
- ), + getCloudDefendRouter: () => CloudDefendRouter, }; } From 218556b765c5dfaa70062fa2c59f751ae04f6aad Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Thu, 16 Feb 2023 23:01:31 +0000 Subject: [PATCH 06/19] policies and status route tests passing. --- .../use_cloud_defend_integration_links.ts | 50 +++++ .../test/fixtures/cloud_defend_integration.ts | 61 ++++++ .../public/test/fixtures/navigation_item.ts | 24 +++ .../public/test/fixtures/react_query.ts | 48 +++++ x-pack/plugins/cloud_defend/server/mocks.ts | 36 ++++ .../server/routes/policies/policies.test.ts | 104 ++++------ .../server/routes/status/status.test.ts | 183 +++++++++--------- .../server/routes/status/status.ts | 1 + 8 files changed, 352 insertions(+), 155 deletions(-) create mode 100644 x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_links.ts create mode 100644 x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts create mode 100644 x-pack/plugins/cloud_defend/public/test/fixtures/navigation_item.ts create mode 100644 x-pack/plugins/cloud_defend/public/test/fixtures/react_query.ts create mode 100644 x-pack/plugins/cloud_defend/server/mocks.ts diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_links.ts b/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_links.ts new file mode 100644 index 0000000000000..b2d0cca31fcba --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/navigation/use_cloud_defend_integration_links.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pagePathGetters, pkgKeyFromPackageInfo } from '@kbn/fleet-plugin/public'; +import { INTEGRATION_PACKAGE_NAME } from '../../../common/constants'; +import { useCloudDefendIntegration } from '../api/use_cloud_defend_integration'; +import { useKibana } from '../hooks/use_kibana'; + +export const useCloudDefendIntegrationLinks = (): { + addIntegrationLink: string | undefined; + docsLink: string; +} => { + const { http } = useKibana().services; + const cloudDefendIntegration = useCloudDefendIntegration(); + + if (!cloudDefendIntegration.isSuccess) + return { + addIntegrationLink: undefined, + docsLink: 'https://www.elastic.co/guide/index.html', + }; + + const addIntegrationLink = pagePathGetters + .add_integration_to_policy({ + integration: INTEGRATION_PACKAGE_NAME, + pkgkey: pkgKeyFromPackageInfo({ + name: cloudDefendIntegration.data.item.name, + version: cloudDefendIntegration.data.item.version, + }), + }) + .join(''); + + const docsLink = pagePathGetters + .integration_details_overview({ + integration: INTEGRATION_PACKAGE_NAME, + pkgkey: pkgKeyFromPackageInfo({ + name: cloudDefendIntegration.data.item.name, + version: cloudDefendIntegration.data.item.version, + }), + }) + .join(''); + + return { + addIntegrationLink: http.basePath.prepend(addIntegrationLink), + docsLink: http.basePath.prepend(docsLink), + }; +}; diff --git a/x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts b/x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts new file mode 100644 index 0000000000000..7ed71008fa113 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import Chance from 'chance'; +import type { CloudDefendPolicy } from '../../../common/types'; + +type CreateCloudDefendIntegrationFixtureInput = { + chance?: Chance.Chance; +} & Partial; + +export const createCloudDefendIntegrationFixture = ({ + chance = new Chance(), + package_policy = { + revision: chance?.integer(), + enabled: true, + id: chance.guid(), + name: chance.string(), + policy_id: chance.guid(), + namespace: chance.string(), + updated_at: chance.date().toISOString(), + updated_by: chance.word(), + created_at: chance.date().toISOString(), + created_by: chance.word(), + inputs: [ + { + type: 'cloudbeat/cis_k8s', + policy_template: 'kspm', + enabled: true, + streams: [ + { + id: chance?.guid(), + enabled: true, + data_stream: { + type: 'logs', + dataset: 'cloud_security_posture.findings', + }, + }, + ], + }, + ], + package: { + name: chance.string(), + title: chance.string(), + version: chance.string(), + }, + }, + agent_policy = { + id: chance.guid(), + name: chance.sentence(), + agents: chance.integer({ min: 0 }), + }, +}: CreateCloudDefendIntegrationFixtureInput = {}): CloudDefendPolicy => ({ + package_policy, + agent_policy, +}); diff --git a/x-pack/plugins/cloud_defend/public/test/fixtures/navigation_item.ts b/x-pack/plugins/cloud_defend/public/test/fixtures/navigation_item.ts new file mode 100644 index 0000000000000..74fdbce35f95b --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/test/fixtures/navigation_item.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import Chance from 'chance'; +import type { CloudDefendPageNavigationItem } from '../../common/navigation/types'; + +type CreateNavigationItemFixtureInput = { + chance?: Chance.Chance; +} & Partial; +export const createPageNavigationItemFixture = ({ + chance = new Chance(), + name = chance.word(), + path = `/${chance.word()}`, + disabled = undefined, + id = 'cloud_defend-policies', +}: CreateNavigationItemFixtureInput = {}): CloudDefendPageNavigationItem => ({ + name, + path, + disabled, + id, +}); diff --git a/x-pack/plugins/cloud_defend/public/test/fixtures/react_query.ts b/x-pack/plugins/cloud_defend/public/test/fixtures/react_query.ts new file mode 100644 index 0000000000000..9605515618882 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/test/fixtures/react_query.ts @@ -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 type { UseQueryResult } from '@tanstack/react-query'; + +interface CreateReactQueryResponseInput { + status?: UseQueryResult['status'] | 'idle'; + data?: TData; + error?: TError; +} + +// TODO: Consider alternatives to using `Partial` over `UseQueryResult` for the return type: +// 1. Fully mock `UseQueryResult` +// 2. Mock the network layer instead of `useQuery` - see: https://tkdodo.eu/blog/testing-react-query +export const createReactQueryResponse = ({ + status = 'loading', + error = undefined, + data = undefined, +}: CreateReactQueryResponseInput = {}): Partial> => { + if (status === 'success') { + return { status, data, isSuccess: true, isLoading: false, isError: false }; + } + + if (status === 'error') { + return { status, error, isSuccess: false, isLoading: false, isError: true }; + } + + if (status === 'loading') { + return { status, data: undefined, isSuccess: false, isLoading: true, isError: false }; + } + + if (status === 'idle') { + return { + status: 'loading', + data: undefined, + isSuccess: false, + isLoading: true, + isError: false, + fetchStatus: 'idle', + }; + } + + return { status }; +}; diff --git a/x-pack/plugins/cloud_defend/server/mocks.ts b/x-pack/plugins/cloud_defend/server/mocks.ts new file mode 100644 index 0000000000000..a3a1fb895e3c8 --- /dev/null +++ b/x-pack/plugins/cloud_defend/server/mocks.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { coreMock } from '@kbn/core/server/mocks'; +import { + createFleetRequestHandlerContextMock, + createMockAgentService, + createMockAgentPolicyService, + createPackagePolicyServiceMock, + createMockPackageService, +} from '@kbn/fleet-plugin/server/mocks'; +import { mockAuthenticatedUser } from '@kbn/security-plugin/common/model/authenticated_user.mock'; + +export const createCloudDefendRequestHandlerContextMock = () => { + const coreMockRequestContext = coreMock.createRequestHandlerContext(); + + return { + core: coreMockRequestContext, + fleet: createFleetRequestHandlerContextMock(), + cloudDefend: { + user: mockAuthenticatedUser(), + logger: loggingSystemMock.createLogger(), + esClient: coreMockRequestContext.elasticsearch.client, + soClient: coreMockRequestContext.savedObjects.client, + agentPolicyService: createMockAgentPolicyService(), + agentService: createMockAgentService(), + packagePolicyService: createPackagePolicyServiceMock(), + packageService: createMockPackageService(), + }, + }; +}; diff --git a/x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts b/x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts index d8b432bb0391e..51dbabe1d936f 100644 --- a/x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts +++ b/x-pack/plugins/cloud_defend/server/routes/policies/policies.test.ts @@ -6,25 +6,25 @@ */ import { httpServerMock, httpServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { - benchmarksQueryParamsSchema, - DEFAULT_BENCHMARKS_PER_PAGE, -} from '../../../common/schemas/benchmark'; + policiesQueryParamsSchema, + DEFAULT_POLICIES_PER_PAGE, +} from '../../../common/schemas/policy'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, - getCspPackagePolicies, - getCspAgentPolicies, + getCloudDefendPackagePolicies, + getCloudDefendAgentPolicies, } from '../../lib/fleet_util'; -import { defineGetBenchmarksRoute, getRulesCountForPolicy } from './benchmarks'; +import { defineGetPoliciesRoute } from './policies'; -import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; +import { SavedObjectsClientContract } from '@kbn/core/server'; import { createMockAgentPolicyService, createPackagePolicyServiceMock, } from '@kbn/fleet-plugin/server/mocks'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; -import { createCspRequestHandlerContextMock } from '../../mocks'; +import { createCloudDefendRequestHandlerContextMock } from '../../mocks'; -describe('benchmarks API', () => { +describe('policies API', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -32,21 +32,21 @@ describe('benchmarks API', () => { it('validate the API route path', async () => { const router = httpServiceMock.createRouter(); - defineGetBenchmarksRoute(router); + defineGetPoliciesRoute(router); const [config] = router.get.mock.calls[0]; - expect(config.path).toEqual('/internal/cloud_security_posture/benchmarks'); + expect(config.path).toEqual('/internal/cloud_defend/policies'); }); it('should accept to a user with fleet.all privilege', async () => { const router = httpServiceMock.createRouter(); - defineGetBenchmarksRoute(router); + defineGetPoliciesRoute(router); const [_, handler] = router.get.mock.calls[0]; - const mockContext = createCspRequestHandlerContextMock(); + const mockContext = createCloudDefendRequestHandlerContextMock(); const mockResponse = httpServerMock.createResponseFactory(); const mockRequest = httpServerMock.createKibanaRequest(); const [context, req, res] = [mockContext, mockRequest, mockResponse]; @@ -59,11 +59,11 @@ describe('benchmarks API', () => { it('should reject to a user without fleet.all privilege', async () => { const router = httpServiceMock.createRouter(); - defineGetBenchmarksRoute(router); + defineGetPoliciesRoute(router); const [_, handler] = router.get.mock.calls[0]; - const mockContext = createCspRequestHandlerContextMock(); + const mockContext = createCloudDefendRequestHandlerContextMock(); mockContext.fleet.authz.fleet.all = false; const mockResponse = httpServerMock.createResponseFactory(); @@ -77,76 +77,76 @@ describe('benchmarks API', () => { describe('test input schema', () => { it('expect to find default values', async () => { - const validatedQuery = benchmarksQueryParamsSchema.validate({}); + const validatedQuery = policiesQueryParamsSchema.validate({}); expect(validatedQuery).toMatchObject({ page: 1, - per_page: DEFAULT_BENCHMARKS_PER_PAGE, + per_page: DEFAULT_POLICIES_PER_PAGE, }); }); - it('expect to find benchmark_name', async () => { - const validatedQuery = benchmarksQueryParamsSchema.validate({ - benchmark_name: 'my_cis_benchmark', + it('expect to find policy_name', async () => { + const validatedQuery = policiesQueryParamsSchema.validate({ + policy_name: 'my_cis_policy', }); expect(validatedQuery).toMatchObject({ page: 1, - per_page: DEFAULT_BENCHMARKS_PER_PAGE, - benchmark_name: 'my_cis_benchmark', + per_page: DEFAULT_POLICIES_PER_PAGE, + policy_name: 'my_cis_policy', }); }); it('should throw when page field is not a positive integer', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ page: -2 }); + policiesQueryParamsSchema.validate({ page: -2 }); }).toThrow(); }); it('should throw when per_page field is not a positive integer', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ per_page: -2 }); + policiesQueryParamsSchema.validate({ per_page: -2 }); }).toThrow(); }); }); it('should throw when sort_field is not string', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ sort_field: true }); + policiesQueryParamsSchema.validate({ sort_field: true }); }).toThrow(); }); it('should not throw when sort_field is a string', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ sort_field: 'package_policy.name' }); + policiesQueryParamsSchema.validate({ sort_field: 'package_policy.name' }); }).not.toThrow(); }); it('should throw when sort_order is not `asc` or `desc`', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ sort_order: 'Other Direction' }); + policiesQueryParamsSchema.validate({ sort_order: 'Other Direction' }); }).toThrow(); }); it('should not throw when `asc` is input for sort_order field', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ sort_order: 'asc' }); + policiesQueryParamsSchema.validate({ sort_order: 'asc' }); }).not.toThrow(); }); it('should not throw when `desc` is input for sort_order field', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ sort_order: 'desc' }); + policiesQueryParamsSchema.validate({ sort_order: 'desc' }); }).not.toThrow(); }); it('should not throw when fields is a known string literal', async () => { expect(() => { - benchmarksQueryParamsSchema.validate({ sort_field: 'package_policy.name' }); + policiesQueryParamsSchema.validate({ sort_field: 'package_policy.name' }); }).not.toThrow(); }); - describe('test benchmarks utils', () => { + describe('test policies utils', () => { let mockSoClient: jest.Mocked; beforeEach(() => { @@ -157,7 +157,7 @@ describe('benchmarks API', () => { it('should format request by package name', async () => { const mockPackagePolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockPackagePolicyService, 'myPackage', { + await getCloudDefendPackagePolicies(mockSoClient, mockPackagePolicyService, 'myPackage', { page: 1, per_page: 100, sort_order: 'desc', @@ -175,7 +175,7 @@ describe('benchmarks API', () => { it('should build sort request by `sort_field` and default `sort_order`', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + await getCloudDefendPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { page: 1, per_page: 100, sort_field: 'package_policy.name', @@ -196,7 +196,7 @@ describe('benchmarks API', () => { it('should build sort request by `sort_field` and asc `sort_order`', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + await getCloudDefendPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { page: 1, per_page: 100, sort_field: 'package_policy.name', @@ -215,19 +215,19 @@ describe('benchmarks API', () => { }); }); - it('should format request by benchmark_name', async () => { + it('should format request by policy_name', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + await getCloudDefendPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { page: 1, per_page: 100, sort_order: 'desc', - benchmark_name: 'my_cis_benchmark', + policy_name: 'cloud_defend_1', }); expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( expect.objectContaining({ - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *my_cis_benchmark*`, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *cloud_defend_1*`, page: 1, perPage: 100, }) @@ -239,7 +239,7 @@ describe('benchmarks API', () => { const agentPolicyService = createMockAgentPolicyService(); const packagePolicies = [createPackagePolicyMock(), createPackagePolicyMock()]; - await getCspAgentPolicies(mockSoClient, packagePolicies, agentPolicyService); + await getCloudDefendAgentPolicies(mockSoClient, packagePolicies, agentPolicyService); expect(agentPolicyService.getByIds.mock.calls[0][1]).toHaveLength(1); }); @@ -252,34 +252,10 @@ describe('benchmarks API', () => { packagePolicy2.policy_id = 'AnotherId'; const packagePolicies = [packagePolicy1, packagePolicy2]; - await getCspAgentPolicies(mockSoClient, packagePolicies, agentPolicyService); + await getCloudDefendAgentPolicies(mockSoClient, packagePolicies, agentPolicyService); expect(agentPolicyService.getByIds.mock.calls[0][1]).toHaveLength(2); }); }); - - describe('test addPackagePolicyCspRuleTemplates', () => { - it('should retrieve the rules count by the filtered benchmark type', async () => { - const benchmark = 'cis_k8s'; - mockSoClient.find.mockResolvedValueOnce({ - aggregations: { enabled_status: { doc_count: 2 } }, - page: 1, - per_page: 10000, - total: 3, - saved_objects: [ - { - type: 'csp_rule', - id: '0af387d0-c933-11ec-b6c8-4f8afc058cc3', - }, - ], - } as unknown as SavedObjectsFindResponse); - - const rulesCount = await getRulesCountForPolicy(mockSoClient, benchmark); - - const expectedFilter = `csp-rule-template.attributes.metadata.benchmark.id: "${benchmark}"`; - expect(mockSoClient.find.mock.calls[0][0].filter).toEqual(expectedFilter); - expect(rulesCount).toEqual(3); - }); - }); }); }); diff --git a/x-pack/plugins/cloud_defend/server/routes/status/status.test.ts b/x-pack/plugins/cloud_defend/server/routes/status/status.test.ts index 98c6536c277d3..124d8f4738cb2 100644 --- a/x-pack/plugins/cloud_defend/server/routes/status/status.test.ts +++ b/x-pack/plugins/cloud_defend/server/routes/status/status.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { defineGetCspStatusRoute, INDEX_TIMEOUT_IN_MINUTES } from './status'; +import { defineGetCloudDefendStatusRoute, INDEX_TIMEOUT_IN_MINUTES } from './status'; import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; import type { ESSearchResponse } from '@kbn/es-types'; import { @@ -23,42 +23,42 @@ import { RegistryPackage, } from '@kbn/fleet-plugin/common'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; -import { createCspRequestHandlerContextMock } from '../../mocks'; +import { createCloudDefendRequestHandlerContextMock } from '../../mocks'; import { errors } from '@elastic/elasticsearch'; -const mockCspPackageInfo: Installation = { +const mockCloudDefendPackageInfo: Installation = { verification_status: 'verified', installed_kibana: [], installed_kibana_space_id: 'default', installed_es: [], package_assets: [], - es_index_patterns: { findings: 'logs-cloud_security_posture.findings-*' }, - name: 'cloud_security_posture', - version: '0.0.14', - install_version: '0.0.14', + es_index_patterns: { alerts: 'logs-cloud_defend.alerts-*' }, + name: 'cloud_defend', + version: '1.0.0', + install_version: '1.0.0', install_status: 'installed', install_started_at: '2022-06-16T15:24:58.281Z', install_source: 'registry', }; -const mockLatestCspPackageInfo: RegistryPackage = { +const mockLatestCloudDefendPackageInfo: RegistryPackage = { format_version: 'mock', - name: 'cloud_security_posture', - title: 'CIS Kubernetes Benchmark', - version: '0.0.14', + name: 'cloud_defend', + title: 'Defend for containers (D4C)', + version: '1.0.0', release: 'experimental', - description: 'Check Kubernetes cluster compliance with the Kubernetes CIS benchmark.', + description: 'Container drift prevention', type: 'integration', - download: '/epr/cloud_security_posture/cloud_security_posture-0.0.14.zip', - path: '/package/cloud_security_posture/0.0.14', + download: '/epr/cloud_defend/cloud_defend-1.0.0.zip', + path: '/package/cloud_defend/1.0.0', policy_templates: [], - owner: { github: 'elastic/cloud-security-posture' }, + owner: { github: 'elastic/sec-cloudnative-integrations' }, categories: ['containers', 'kubernetes'], }; -describe('CspSetupStatus route', () => { +describe('CloudDefendSetupStatus route', () => { const router = httpServiceMock.createRouter(); - let mockContext: ReturnType; + let mockContext: ReturnType; let mockPackagePolicyService: jest.Mocked; let mockAgentPolicyService: jest.Mocked; let mockAgentService: jest.Mocked; @@ -69,36 +69,28 @@ describe('CspSetupStatus route', () => { beforeEach(() => { jest.clearAllMocks(); - mockContext = createCspRequestHandlerContextMock(); - mockPackagePolicyService = mockContext.csp.packagePolicyService; - mockAgentPolicyService = mockContext.csp.agentPolicyService; - mockAgentService = mockContext.csp.agentService; - mockPackageService = mockContext.csp.packageService; + mockContext = createCloudDefendRequestHandlerContextMock(); + mockPackagePolicyService = mockContext.cloudDefend.packagePolicyService; + mockAgentPolicyService = mockContext.cloudDefend.agentPolicyService; + mockAgentService = mockContext.cloudDefend.agentService; + mockPackageService = mockContext.cloudDefend.packageService; mockAgentClient = mockAgentService.asInternalUser as jest.Mocked; mockPackageClient = mockPackageService.asInternalUser as jest.Mocked; }); it('validate the API route path', async () => { - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [config, _] = router.get.mock.calls[0]; - expect(config.path).toEqual('/internal/cloud_security_posture/status'); + expect(config.path).toEqual('/internal/cloud_defend/status'); }); const indices = [ { - index: 'logs-cloud_security_posture.findings-default*', + index: 'logs-cloud_defend.alerts-default*', expected_status: 'not-installed', }, - { - index: 'logs-cloud_security_posture.findings_latest-default', - expected_status: 'unprivileged', - }, - { - index: 'logs-cloud_security_posture.scores-default', - expected_status: 'unprivileged', - }, ]; indices.forEach((idxTestCase) => { @@ -128,7 +120,9 @@ describe('CspSetupStatus route', () => { } as any; } ); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -138,7 +132,7 @@ describe('CspSetupStatus route', () => { }); // Act - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; const mockResponse = httpServerMock.createResponseFactory(); @@ -150,20 +144,22 @@ describe('CspSetupStatus route', () => { const body = call[0]?.body; expect(mockResponse.ok).toHaveBeenCalledTimes(1); - await expect(body).toMatchObject({ + expect(body).toMatchObject({ status: idxTestCase.expected_status, }); } ); }); - it('Verify the API result when there are findings and no installed policies', async () => { + it('Verify the API result when there are alerts and no installed policies', async () => { mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ hits: { - hits: [{ Findings: 'foo' }], + hits: [{ Alerts: 'foo' }], }, } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -173,7 +169,7 @@ describe('CspSetupStatus route', () => { }); // Act - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; const mockResponse = httpServerMock.createResponseFactory(); @@ -187,23 +183,24 @@ describe('CspSetupStatus route', () => { await expect(body).toMatchObject({ status: 'indexed', - latestPackageVersion: '0.0.14', + latestPackageVersion: '1.0.0', installedPackagePolicies: 0, healthyAgents: 0, installedPackageVersion: undefined, - isPluginInitialized: false, }); }); - it('Verify the API result when there are findings, installed policies, no running agents', async () => { + it('Verify the API result when there are alerts, installed policies, no running agents', async () => { mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ hits: { - hits: [{ Findings: 'foo' }], + hits: [{ Alerts: 'foo' }], }, } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCloudDefendPackageInfo); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -213,7 +210,7 @@ describe('CspSetupStatus route', () => { }); // Act - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; const mockResponse = httpServerMock.createResponseFactory(); @@ -228,23 +225,24 @@ describe('CspSetupStatus route', () => { await expect(body).toMatchObject({ status: 'indexed', - latestPackageVersion: '0.0.14', + latestPackageVersion: '1.0.0', installedPackagePolicies: 3, healthyAgents: 0, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, + installedPackageVersion: '1.0.0', }); }); - it('Verify the API result when there are findings, installed policies, running agents', async () => { + it('Verify the API result when there are alerts, installed policies, running agents', async () => { mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ hits: { - hits: [{ Findings: 'foo' }], + hits: [{ Alerts: 'foo' }], }, } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCloudDefendPackageInfo); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -263,7 +261,7 @@ describe('CspSetupStatus route', () => { } as unknown as GetAgentStatusResponse['results']); // Act - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; const mockResponse = httpServerMock.createResponseFactory(); @@ -276,23 +274,24 @@ describe('CspSetupStatus route', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); - await expect(body).toMatchObject({ + expect(body).toMatchObject({ status: 'indexed', - latestPackageVersion: '0.0.14', + latestPackageVersion: '1.0.0', installedPackagePolicies: 3, healthyAgents: 1, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, + installedPackageVersion: '1.0.0', }); }); - it('Verify the API result when there are no findings and no installed policies', async () => { + it('Verify the API result when there are no alerts and no installed policies', async () => { mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ hits: { hits: [], }, } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -300,7 +299,7 @@ describe('CspSetupStatus route', () => { page: 1, perPage: 100, }); - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; const mockResponse = httpServerMock.createResponseFactory(); @@ -315,24 +314,25 @@ describe('CspSetupStatus route', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); - await expect(body).toMatchObject({ + expect(body).toMatchObject({ status: 'not-installed', - latestPackageVersion: '0.0.14', + latestPackageVersion: '1.0.0', installedPackagePolicies: 0, healthyAgents: 0, - isPluginInitialized: false, }); }); - it('Verify the API result when there are no findings, installed agent but no deployed agent', async () => { + it('Verify the API result when there are no alerts, installed agent but no deployed agent', async () => { mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ hits: { hits: [], }, } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCloudDefendPackageInfo); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -351,7 +351,7 @@ describe('CspSetupStatus route', () => { } as unknown as GetAgentStatusResponse['results']); // Act - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; @@ -365,30 +365,31 @@ describe('CspSetupStatus route', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); - await expect(body).toMatchObject({ + expect(body).toMatchObject({ status: 'not-deployed', - latestPackageVersion: '0.0.14', + latestPackageVersion: '1.0.0', installedPackagePolicies: 1, healthyAgents: 0, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, + installedPackageVersion: '1.0.0', }); }); - it('Verify the API result when there are no findings, installed agent, deployed agent, before index timeout', async () => { + it('Verify the API result when there are no alerts, installed agent, deployed agent, before index timeout', async () => { mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ hits: { hits: [], }, } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); const currentTime = new Date(); - mockCspPackageInfo.install_started_at = new Date( + mockCloudDefendPackageInfo.install_started_at = new Date( currentTime.setMinutes(currentTime.getMinutes() - INDEX_TIMEOUT_IN_MINUTES + 1) ).toUTCString(); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCloudDefendPackageInfo); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -407,7 +408,7 @@ describe('CspSetupStatus route', () => { } as unknown as GetAgentStatusResponse['results']); // Act - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; @@ -423,30 +424,31 @@ describe('CspSetupStatus route', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); - await expect(body).toMatchObject({ + expect(body).toMatchObject({ status: 'indexing', - latestPackageVersion: '0.0.14', + latestPackageVersion: '1.0.0', installedPackagePolicies: 1, healthyAgents: 1, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, + installedPackageVersion: '1.0.0', }); }); - it('Verify the API result when there are no findings, installed agent, deployed agent, after index timeout', async () => { + it('Verify the API result when there are no alerts, installed agent, deployed agent, after index timeout', async () => { mockContext.core.elasticsearch.client.asCurrentUser.search.mockResponseOnce({ hits: { hits: [], }, } as unknown as ESSearchResponse); - mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce(mockLatestCspPackageInfo); + mockPackageClient.fetchFindLatestPackage.mockResolvedValueOnce( + mockLatestCloudDefendPackageInfo + ); const currentTime = new Date(); - mockCspPackageInfo.install_started_at = new Date( + mockCloudDefendPackageInfo.install_started_at = new Date( currentTime.setMinutes(currentTime.getMinutes() - INDEX_TIMEOUT_IN_MINUTES - 1) ).toUTCString(); - mockPackageClient.getInstallation.mockResolvedValueOnce(mockCspPackageInfo); + mockPackageClient.getInstallation.mockResolvedValueOnce(mockCloudDefendPackageInfo); mockPackagePolicyService.list.mockResolvedValueOnce({ items: [], @@ -465,7 +467,7 @@ describe('CspSetupStatus route', () => { } as unknown as GetAgentStatusResponse['results']); // Act - defineGetCspStatusRoute(router); + defineGetCloudDefendStatusRoute(router); const [_, handler] = router.get.mock.calls[0]; @@ -480,13 +482,12 @@ describe('CspSetupStatus route', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); - await expect(body).toMatchObject({ + expect(body).toMatchObject({ status: 'index-timeout', - latestPackageVersion: '0.0.14', + latestPackageVersion: '1.0.0', installedPackagePolicies: 1, healthyAgents: 1, - installedPackageVersion: '0.0.14', - isPluginInitialized: false, + installedPackageVersion: '1.0.0', }); }); }); diff --git a/x-pack/plugins/cloud_defend/server/routes/status/status.ts b/x-pack/plugins/cloud_defend/server/routes/status/status.ts index 438d9176cea31..74a43a8c2a6dd 100644 --- a/x-pack/plugins/cloud_defend/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_defend/server/routes/status/status.ts @@ -72,6 +72,7 @@ const calculateCloudDefendStatusCode = ( ): CloudDefendStatusCode => { // We check privileges only for the relevant indices for our pages to appear if (indicesStatus.alerts === 'unprivileged') return 'unprivileged'; + if (indicesStatus.alerts === 'not-empty') return 'indexed'; if (installedCloudDefendPackagePolicies === 0) return 'not-installed'; if (healthyAgents === 0) return 'not-deployed'; if (timeSinceInstallationInMinutes <= INDEX_TIMEOUT_IN_MINUTES) return 'indexing'; From 4d229339dabc88e0ded8e2f712595660d428e621 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Thu, 16 Feb 2023 23:29:16 +0000 Subject: [PATCH 07/19] backend tests and subscription check code working --- .../cloud_defend/common/utils/subscription.ts | 2 +- .../public/application/setup_context.ts | 16 ++++++ .../common/hooks/use_subscription_status.ts | 22 ++++++++ .../cloud_defend_page/index.test.tsx | 11 ++-- .../components/cloud_defend_page/index.tsx | 15 +++--- .../subscription_not_allowed/index.tsx | 52 +++++++++++++++++++ .../public/pages/policies/index.test.tsx | 8 +-- x-pack/plugins/cloud_defend/public/plugin.tsx | 25 +++++++-- x-pack/plugins/cloud_defend/public/types.ts | 5 +- 9 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/cloud_defend/public/application/setup_context.ts create mode 100644 x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts create mode 100644 x-pack/plugins/cloud_defend/public/components/subscription_not_allowed/index.tsx diff --git a/x-pack/plugins/cloud_defend/common/utils/subscription.ts b/x-pack/plugins/cloud_defend/common/utils/subscription.ts index 2d9707681e047..e54eb0c4d4581 100644 --- a/x-pack/plugins/cloud_defend/common/utils/subscription.ts +++ b/x-pack/plugins/cloud_defend/common/utils/subscription.ts @@ -6,7 +6,7 @@ */ import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; -import { PLUGIN_NAME } from '..'; +import { PLUGIN_NAME } from '../constants'; const MINIMUM_NON_CLOUD_LICENSE_TYPE: LicenseType = 'enterprise'; diff --git a/x-pack/plugins/cloud_defend/public/application/setup_context.ts b/x-pack/plugins/cloud_defend/public/application/setup_context.ts new file mode 100644 index 0000000000000..574404ace38ce --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/application/setup_context.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createContext } from 'react'; + +interface SetupContextValue { + isCloudEnabled?: boolean; +} + +/** + * A utility to pass data from the plugin setup lifecycle stage to application components + */ +export const SetupContext = createContext({}); diff --git a/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts b/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts new file mode 100644 index 0000000000000..f8bda84dbcb65 --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts @@ -0,0 +1,22 @@ +/* + * 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 { useContext } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { SetupContext } from '../../application/setup_context'; +import { isSubscriptionAllowed } from '../../../common/utils/subscription'; +import { useKibana } from './use_kibana'; + +const SUBSCRIPTION_QUERY_KEY = 'csp_subscription_query_key'; + +export const useSubscriptionStatus = () => { + const { licensing } = useKibana().services; + const { isCloudEnabled } = useContext(SetupContext); + return useQuery([SUBSCRIPTION_QUERY_KEY], async () => { + const license = await licensing.refresh(); + return isSubscriptionAllowed(isCloudEnabled, license); + }); +}; diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx index 6394e805a07c8..4c620a73dcfa9 100644 --- a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -// import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; +import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; import Chance from 'chance'; import { CloudDefendPage, @@ -29,7 +29,7 @@ import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_clou const chance = new Chance(); jest.mock('../../common/api/use_setup_status_api'); -// jest.mock('../../common/hooks/use_subscription_status'); +jest.mock('../../common/hooks/use_subscription_status'); jest.mock('../../common/navigation/use_cloud_defend_integration_links'); describe('', () => { @@ -47,12 +47,12 @@ describe('', () => { docsLink: chance.url(), })); - /* (useSubscriptionStatus as jest.Mock).mockImplementation(() => + (useSubscriptionStatus as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', data: true, }) - );*/ + ); }); const renderCloudDefendPage = ( @@ -90,7 +90,7 @@ describe('', () => { expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument(); }); - /* it('renders default loading state when the subscription query is loading', () => { + it('renders default loading state when the subscription query is loading', () => { (useSubscriptionStatus as jest.Mock).mockImplementation( () => createReactQueryResponse({ @@ -144,7 +144,6 @@ describe('', () => { expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument(); expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument(); }); - */ it('renders integrations installation prompt if integration is not installed', () => { (useCloudDefendSetupStatusApi as jest.Mock).mockImplementation(() => diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx index 71b5a752d17ce..e55f5274710ec 100644 --- a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx @@ -18,8 +18,8 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { NoDataPage, NoDataPageProps } from '@kbn/kibana-react-plugin/public'; import { css } from '@emotion/react'; -// import { SubscriptionNotAllowed } from './subscription_not_allowed'; -// import { useSubscriptionStatus } from '../common/hooks/use_subscription_status'; +import { SubscriptionNotAllowed } from '../subscription_not_allowed'; +import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; import { FullSizeCenteredPage } from '../full_size_page'; import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api'; import { LoadingState } from '../loading_state'; @@ -221,11 +221,11 @@ const defaultNoDataRenderer = (docsLink: string) => (
); -/* const subscriptionNotAllowedRenderer = () => ( +const subscriptionNotAllowedRenderer = () => ( -); */ +); interface CloudPosturePageProps { children: React.ReactNode; @@ -242,13 +242,12 @@ export const CloudDefendPage = ({ errorRender = defaultErrorRenderer, noDataRenderer = defaultNoDataRenderer, }: CloudPosturePageProps) => { - // const subscriptionStatus = useSubscriptionStatus(); + const subscriptionStatus = useSubscriptionStatus(); const getSetupStatus = useCloudDefendSetupStatusApi(); const { addIntegrationLink, docsLink } = useCloudDefendIntegrationLinks(); const render = () => { - // TODO: subscription status work.. - /* if (subscriptionStatus.isError) { + if (subscriptionStatus.isError) { return defaultErrorRenderer(subscriptionStatus.error); } @@ -258,7 +257,7 @@ export const CloudDefendPage = ({ if (!subscriptionStatus.data) { return subscriptionNotAllowedRenderer(); - }*/ + } if (getSetupStatus.isError) { return defaultErrorRenderer(getSetupStatus.error); diff --git a/x-pack/plugins/cloud_defend/public/components/subscription_not_allowed/index.tsx b/x-pack/plugins/cloud_defend/public/components/subscription_not_allowed/index.tsx new file mode 100644 index 0000000000000..7ab4afa3fb06e --- /dev/null +++ b/x-pack/plugins/cloud_defend/public/components/subscription_not_allowed/index.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt, EuiPageSection, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { useKibana } from '../../common/hooks/use_kibana'; + +export const SubscriptionNotAllowed = () => { + const { application } = useKibana().services; + return ( + + + + + } + body={ +

+ + + + ), + }} + /> +

+ } + /> +
+ ); +}; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx index 41f836f6540d1..61fcf6f0a844a 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.test.tsx @@ -15,12 +15,12 @@ import { Policies } from '.'; import * as TEST_SUBJ from './test_subjects'; import { useCloudDefendPolicies } from './use_cloud_defend_policies'; import { useCloudDefendSetupStatusApi } from '../../common/api/use_setup_status_api'; -// import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; +import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status'; import { useCloudDefendIntegrationLinks } from '../../common/navigation/use_cloud_defend_integration_links'; jest.mock('./use_cloud_defend_policies'); jest.mock('../../common/api/use_setup_status_api'); -// jest.mock('../../common/hooks/use_subscription_status'); +jest.mock('../../common/hooks/use_subscription_status'); jest.mock('../../common/navigation/use_cloud_defend_integration_links'); const chance = new Chance(); @@ -35,12 +35,12 @@ describe('', () => { }) ); - /* (useSubscriptionStatus as jest.Mock).mockImplementation(() => + (useSubscriptionStatus as jest.Mock).mockImplementation(() => createReactQueryResponse({ status: 'success', data: true, }) - ); */ + ); (useCloudDefendIntegrationLinks as jest.Mock).mockImplementation(() => ({ addIntegrationLink: chance.url(), diff --git a/x-pack/plugins/cloud_defend/public/plugin.tsx b/x-pack/plugins/cloud_defend/public/plugin.tsx index f52c9bef2a34e..b9272993e6b55 100755 --- a/x-pack/plugins/cloud_defend/public/plugin.tsx +++ b/x-pack/plugins/cloud_defend/public/plugin.tsx @@ -13,9 +13,11 @@ import { CloudDefendPluginSetup, CloudDefendPluginStart, CloudDefendPluginStartDeps, + CloudDefendPluginSetupDeps, } from './types'; import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; import { LoadingState } from './components/loading_state'; +import { SetupContext } from './application/setup_context'; const LazyEditPolicy = lazy(() => import('./components/fleet_extensions/policy_extension_edit')); const LazyCreatePolicy = lazy( @@ -29,8 +31,23 @@ const Router = (props: CloudDefendRouterProps) => ( ); -export class CloudDefendPlugin implements Plugin { - public setup(core: CoreSetup): CloudDefendPluginSetup { +export class CloudDefendPlugin + implements + Plugin< + CloudDefendPluginSetup, + CloudDefendPluginStart, + CloudDefendPluginSetupDeps, + CloudDefendPluginStartDeps + > +{ + private isCloudEnabled?: boolean; + + public setup( + core: CoreSetup, + plugins: CloudDefendPluginSetupDeps + ): CloudDefendPluginSetup { + this.isCloudEnabled = plugins.cloud.isCloudEnabled; + // Return methods that should be available to other plugins return {}; } @@ -52,7 +69,9 @@ export class CloudDefendPlugin implements Plugin
- + + +
diff --git a/x-pack/plugins/cloud_defend/public/types.ts b/x-pack/plugins/cloud_defend/public/types.ts index 89762b2cdec8e..9dda18ef584ba 100755 --- a/x-pack/plugins/cloud_defend/public/types.ts +++ b/x-pack/plugins/cloud_defend/public/types.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { CloudSetup } from '@kbn/cloud-plugin/public'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { FleetSetup, FleetStart } from '@kbn/fleet-plugin/public'; import { NewPackagePolicy } from '@kbn/fleet-plugin/public'; import type { ComponentType, ReactNode } from 'react'; @@ -28,10 +29,12 @@ export interface CloudDefendPluginStart { export interface CloudDefendPluginSetupDeps { fleet: FleetSetup; + cloud: CloudSetup; usageCollection?: UsageCollectionSetup; } export interface CloudDefendPluginStartDeps { fleet: FleetStart; + licensing: LicensingPluginStart; usageCollection?: UsageCollectionStart; } From 4d54430c2d7a903fe82be1125124d0dc0143447d Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Thu, 16 Feb 2023 23:44:28 +0000 Subject: [PATCH 08/19] cleanup --- x-pack/plugins/cloud_defend/README.md | 1 + x-pack/plugins/cloud_defend/public/pages/policies/index.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_defend/README.md b/x-pack/plugins/cloud_defend/README.md index c0175af9cc2a6..9df1a13d328d2 100755 --- a/x-pack/plugins/cloud_defend/README.md +++ b/x-pack/plugins/cloud_defend/README.md @@ -42,6 +42,7 @@ responses: ``` node scripts/type_check.js --project x-pack/plugins/cloud_defend/tsconfig.json +node scripts/eslint.js x-pack/plugins/cloud_defend yarn test:jest x-pack/plugins/cloud_defend ``` diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx index 3c351c57ecdd2..2c0d7c87c3a58 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx @@ -144,7 +144,7 @@ export const Policies = () => { pageTitle={ } From 3bc725e3d63d94d2a39319244029588eac1daa5f Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 16 Feb 2023 23:52:01 +0000 Subject: [PATCH 09/19] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/cloud_defend/tsconfig.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_defend/tsconfig.json b/x-pack/plugins/cloud_defend/tsconfig.json index 1924eeaf64ce6..41e082c83bb54 100755 --- a/x-pack/plugins/cloud_defend/tsconfig.json +++ b/x-pack/plugins/cloud_defend/tsconfig.json @@ -18,8 +18,11 @@ "@kbn/i18n", "@kbn/usage-collection-plugin", "@kbn/cloud-plugin", - "@kbn/unified-search-plugin", - "@kbn/shared-ux-router" + "@kbn/shared-ux-router", + "@kbn/shared-ux-link-redirect-app", + "@kbn/core-logging-server-mocks", + "@kbn/securitysolution-es-utils", + "@kbn/es-types" ], "exclude": ["target/**/*"] } From 277c441ea7702821ae545cf1e1b8ad07bcdd4c99 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Mon, 20 Feb 2023 18:21:56 +0000 Subject: [PATCH 10/19] fixed type errors --- .../cloud_defend/common/utils/helpers.ts | 4 ++++ .../public/pages/policies/index.tsx | 1 + x-pack/plugins/cloud_defend/server/plugin.ts | 20 ++++++++++++++++--- x-pack/plugins/cloud_defend/server/types.ts | 2 ++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cloud_defend/common/utils/helpers.ts b/x-pack/plugins/cloud_defend/common/utils/helpers.ts index 2fdc1087e54d2..14b506e8d2e70 100644 --- a/x-pack/plugins/cloud_defend/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_defend/common/utils/helpers.ts @@ -6,6 +6,7 @@ */ import { Truthy } from 'lodash'; +import { INTEGRATION_PACKAGE_NAME } from '../constants'; /** * @example @@ -29,3 +30,6 @@ export function assert(condition: any, msg?: string): asserts condition { throw new Error(msg); } } + +export const isCloudDefendPackage = (packageName?: string) => + packageName === INTEGRATION_PACKAGE_NAME; diff --git a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx index 2c0d7c87c3a58..c732be5421a17 100644 --- a/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx +++ b/x-pack/plugins/cloud_defend/public/pages/policies/index.tsx @@ -170,6 +170,7 @@ export const Policies = () => { pageIndex={query.page} pageSize={pageSize || query.perPage} sorting={{ + // @ts-expect-error - EUI types currently do not support sorting by nested fields sort: { field: query.sortField, direction: query.sortOrder }, allowNeutralSort: false, }} diff --git a/x-pack/plugins/cloud_defend/server/plugin.ts b/x-pack/plugins/cloud_defend/server/plugin.ts index 246fc338b3e8c..05926e82cf796 100644 --- a/x-pack/plugins/cloud_defend/server/plugin.ts +++ b/x-pack/plugins/cloud_defend/server/plugin.ts @@ -5,14 +5,16 @@ * 2.0. */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '@kbn/core/server'; +import type { NewPackagePolicy } from '@kbn/fleet-plugin/common'; import { CloudDefendPluginSetup, CloudDefendPluginStart, CloudDefendPluginStartDeps, CloudDefendPluginSetupDeps, } from './types'; -import { INTEGRATION_PACKAGE_NAME } from '../common/constants'; import { setupRoutes } from './routes/setup_routes'; +import { isCloudDefendPackage } from '../common/utils/helpers'; +import { isSubscriptionAllowed } from '../common/utils/subscription'; export class CloudDefendPlugin implements Plugin { private readonly logger: Logger; @@ -42,8 +44,20 @@ export class CloudDefendPlugin implements Plugin { - const packageInfo = await plugins.fleet.packageService.asInternalUser.getInstallation( - INTEGRATION_PACKAGE_NAME + plugins.fleet.registerExternalCallback( + 'packagePolicyCreate', + async (packagePolicy: NewPackagePolicy): Promise => { + const license = await plugins.licensing.refresh(); + if (isCloudDefendPackage(packagePolicy.package?.name)) { + if (!isSubscriptionAllowed(this.isCloudEnabled, license)) { + throw new Error( + 'To use this feature you must upgrade your subscription or start a trial' + ); + } + } + + return packagePolicy; + } ); }); diff --git a/x-pack/plugins/cloud_defend/server/types.ts b/x-pack/plugins/cloud_defend/server/types.ts index b624c1793ee0a..121a97ee926be 100644 --- a/x-pack/plugins/cloud_defend/server/types.ts +++ b/x-pack/plugins/cloud_defend/server/types.ts @@ -13,6 +13,7 @@ import type { SavedObjectsClientContract, IScopedClusterClient, } from '@kbn/core/server'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; import type { FleetStartContract, FleetRequestHandlerContext, @@ -40,6 +41,7 @@ export interface CloudDefendPluginStartDeps { data: DataPluginStart; fleet: FleetStartContract; security: SecurityPluginStart; + licensing: LicensingPluginStart; } export interface CloudDefendApiRequestHandlerContext { From 3cde78aa5050a5836c4343302ceac1961871c4aa Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Mon, 20 Feb 2023 19:17:58 +0000 Subject: [PATCH 11/19] type check fixes --- x-pack/plugins/cloud_defend/public/index.ts | 5 ++++- .../security_solution/public/app/home/home_navigations.ts | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_defend/public/index.ts b/x-pack/plugins/cloud_defend/public/index.ts index 9dcf8bd5b2760..b74c04111c91b 100755 --- a/x-pack/plugins/cloud_defend/public/index.ts +++ b/x-pack/plugins/cloud_defend/public/index.ts @@ -7,7 +7,10 @@ import { CloudDefendPlugin } from './plugin'; export type { CloudDefendSecuritySolutionContext } from './types'; -export { getSecuritySolutionLink } from './common/navigation/security_solution_links'; +export { + getSecuritySolutionLink, + getSecuritySolutionNavTab, +} from './common/navigation/security_solution_links'; export { CLOUD_DEFEND_BASE_PATH } from './common/navigation/constants'; export type { CloudDefendPageId } from './common/navigation/types'; diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index bbff6ffa0a6f9..d9a988004ac1a 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -7,6 +7,7 @@ import { getSecuritySolutionNavTab as getSecuritySolutionCSPNavTab } from '@kbn/cloud-security-posture-plugin/public'; import { getSecuritySolutionNavTab as getSecuritySolutionTINavTab } from '@kbn/threat-intelligence-plugin/public'; +import { getSecuritySolutionNavTab as getSecuritySolutionCloudDefendNavTab } from '@kbn/cloud-defend-plugin/public'; import * as i18n from '../translations'; import type { SecurityNav, SecurityNavGroup } from '../../common/components/navigation/types'; import { SecurityNavGroupKey } from '../../common/components/navigation/types'; @@ -186,6 +187,10 @@ export const navTabs: SecurityNav = { ...getSecuritySolutionCSPNavTab('benchmarks', APP_PATH), urlKey: 'administration', }, + [SecurityPageName.cloudDefendPolicies]: { + ...getSecuritySolutionCloudDefendNavTab('policies', APP_PATH), + urlKey: 'administration', + }, [SecurityPageName.entityAnalytics]: { id: SecurityPageName.entityAnalytics, name: i18n.ENTITY_ANALYTICS, From 2c08fdd539f2e767596d1b84904751524665a4c2 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Tue, 21 Feb 2023 18:14:02 +0000 Subject: [PATCH 12/19] fixed dep --- x-pack/plugins/cloud_defend/kibana.jsonc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_defend/kibana.jsonc b/x-pack/plugins/cloud_defend/kibana.jsonc index ac66730bd6993..392724467fd70 100644 --- a/x-pack/plugins/cloud_defend/kibana.jsonc +++ b/x-pack/plugins/cloud_defend/kibana.jsonc @@ -18,7 +18,8 @@ "unifiedSearch", "kibanaReact", "cloud", - "security" + "security", + "licensing" ], "optionalPlugins": [ "usageCollection" From 7df9a71a0d80ce1749eeb3d90444018824b96dfb Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Tue, 21 Feb 2023 22:09:45 +0000 Subject: [PATCH 13/19] cleanup --- x-pack/plugins/cloud_defend/common/types.ts | 5 ++--- .../public/application/router.tsx | 2 +- .../public/common/api/use_setup_status_api.ts | 5 +---- .../common/hooks/use_subscription_status.ts | 2 +- .../navigation/security_solution_links.ts | 4 ++-- .../components/cloud_defend_page/index.tsx | 4 ++-- .../test/fixtures/cloud_defend_integration.ts | 6 +++--- .../plugins/cloud_defend/public/test/mocks.ts | 8 ++++---- .../public/test/test_provider.tsx | 4 ++-- .../cloud_defend/server/lib/fleet_util.ts | 2 +- .../server/routes/status/status.ts | 19 ++----------------- 11 files changed, 21 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/cloud_defend/common/types.ts b/x-pack/plugins/cloud_defend/common/types.ts index 5f2c68576049b..c3bdbb4418183 100644 --- a/x-pack/plugins/cloud_defend/common/types.ts +++ b/x-pack/plugins/cloud_defend/common/types.ts @@ -13,9 +13,9 @@ export type IndexStatus = | 'unprivileged'; // User doesn't have access to query the index export type CloudDefendStatusCode = - | 'indexed' // latest findings index exists and has results + | 'indexed' // alerts index exists and has results | 'indexing' // index timeout was not surpassed since installation, assumes data is being indexed - | 'unprivileged' // user lacks privileges for the latest findings index + | 'unprivileged' // user lacks privileges for the alerts index | 'index-timeout' // index timeout was surpassed since installation | 'not-deployed' // no healthy agents were deployed | 'not-installed'; // number of installed integrations is 0; @@ -38,7 +38,6 @@ interface CloudDefendSetupNotInstalledStatus extends BaseCloudDefendSetupStatus interface CloudDefendSetupInstalledStatus extends BaseCloudDefendSetupStatus { status: Exclude; - // if installedPackageVersion == undefined but status != 'not-installed' it means the integration was installed in the past and findings were found // status can be `indexed` but return with undefined package information in this case installedPackageVersion: string | undefined; } diff --git a/x-pack/plugins/cloud_defend/public/application/router.tsx b/x-pack/plugins/cloud_defend/public/application/router.tsx index 6ffd159c2d09a..42010ea047ca5 100644 --- a/x-pack/plugins/cloud_defend/public/application/router.tsx +++ b/x-pack/plugins/cloud_defend/public/application/router.tsx @@ -19,7 +19,7 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } }, }); -/** Props for the cloud security posture router component */ +/** Props for the cloud_defend router component */ export interface CloudDefendRouterProps { securitySolutionContext?: CloudDefendSecuritySolutionContext; } diff --git a/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts index f65926ec6c98a..e62bc058f22db 100644 --- a/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts +++ b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts @@ -12,13 +12,10 @@ import { STATUS_ROUTE_PATH } from '../../../common/constants'; const getCloudDefendSetupStatusQueryKey = 'cloud_defend_status_key'; -export const useCloudDefendSetupStatusApi = ({ - options, -}: { options?: UseQueryOptions } = {}) => { +export const useCloudDefendSetupStatusApi = () => { const { http } = useKibana().services; return useQuery( [getCloudDefendSetupStatusQueryKey], () => http.get(STATUS_ROUTE_PATH), - options ); }; diff --git a/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts b/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts index f8bda84dbcb65..26dd4caa33c4d 100644 --- a/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts +++ b/x-pack/plugins/cloud_defend/public/common/hooks/use_subscription_status.ts @@ -10,7 +10,7 @@ import { SetupContext } from '../../application/setup_context'; import { isSubscriptionAllowed } from '../../../common/utils/subscription'; import { useKibana } from './use_kibana'; -const SUBSCRIPTION_QUERY_KEY = 'csp_subscription_query_key'; +const SUBSCRIPTION_QUERY_KEY = 'cloud_defend_subscription_query_key'; export const useSubscriptionStatus = () => { const { licensing } = useKibana().services; diff --git a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts index 0ef838437abf3..58e816c135593 100644 --- a/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts +++ b/x-pack/plugins/cloud_defend/public/common/navigation/security_solution_links.ts @@ -22,8 +22,8 @@ interface CloudDefendNavTab { } /** - * Gets the cloud security posture link properties of a CSP page for navigation in the security solution. - * @param cloudDefendPage the name of the cloud posture page. + * Gets the cloud_defend link properties of a Cloud Defend page for navigation in the security solution. + * @param cloudDefendPage the name of the cloud defend page. */ export const getSecuritySolutionLink = ( cloudDefendPage: CloudDefendPage diff --git a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx index e55f5274710ec..bc10db7ace74b 100644 --- a/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx +++ b/x-pack/plugins/cloud_defend/public/components/cloud_defend_page/index.tsx @@ -227,7 +227,7 @@ const subscriptionNotAllowedRenderer = () => ( ); -interface CloudPosturePageProps { +interface CloudDefendPageProps { children: React.ReactNode; query?: UseQueryResult; loadingRender?: () => React.ReactNode; @@ -241,7 +241,7 @@ export const CloudDefendPage = ({ loadingRender = defaultLoadingRenderer, errorRender = defaultErrorRenderer, noDataRenderer = defaultNoDataRenderer, -}: CloudPosturePageProps) => { +}: CloudDefendPageProps) => { const subscriptionStatus = useSubscriptionStatus(); const getSetupStatus = useCloudDefendSetupStatusApi(); const { addIntegrationLink, docsLink } = useCloudDefendIntegrationLinks(); diff --git a/x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts b/x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts index 7ed71008fa113..830977947673c 100644 --- a/x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts +++ b/x-pack/plugins/cloud_defend/public/test/fixtures/cloud_defend_integration.ts @@ -29,8 +29,8 @@ export const createCloudDefendIntegrationFixture = ({ created_by: chance.word(), inputs: [ { - type: 'cloudbeat/cis_k8s', - policy_template: 'kspm', + type: 'cloud_defend/control', + policy_template: 'cloud_defend', enabled: true, streams: [ { @@ -38,7 +38,7 @@ export const createCloudDefendIntegrationFixture = ({ enabled: true, data_stream: { type: 'logs', - dataset: 'cloud_security_posture.findings', + dataset: 'cloud_defend.alerts', }, }, ], diff --git a/x-pack/plugins/cloud_defend/public/test/mocks.ts b/x-pack/plugins/cloud_defend/public/test/mocks.ts index 7dcf8d99d9116..4921d3f6d0f98 100644 --- a/x-pack/plugins/cloud_defend/public/test/mocks.ts +++ b/x-pack/plugins/cloud_defend/public/test/mocks.ts @@ -71,8 +71,8 @@ export const getCloudDefendNewPolicyMock = (yaml = MOCK_YAML_CONFIGURATION): New ], package: { name: 'cloud_defend', - title: 'Kubernetes Security Posture Management', - version: '0.0.21', + title: 'Container drift prevention', + version: '1.0.0', }, }); @@ -114,7 +114,7 @@ export const getCloudDefendPolicyMock = (yaml = MOCK_YAML_CONFIGURATION): Packag ], package: { name: 'cloud_defend', - title: 'Kubernetes Security Posture Management', - version: '0.0.21', + title: 'Container drift prevention', + version: '1.0.0', }, }); diff --git a/x-pack/plugins/cloud_defend/public/test/test_provider.tsx b/x-pack/plugins/cloud_defend/public/test/test_provider.tsx index 472f0ea04ecd1..c5deff816241c 100755 --- a/x-pack/plugins/cloud_defend/public/test/test_provider.tsx +++ b/x-pack/plugins/cloud_defend/public/test/test_provider.tsx @@ -37,13 +37,13 @@ Object.defineProperty(window, 'matchMedia', { })), }); -interface CspAppDeps { +interface CloudDefendAppDeps { core: CoreStart; deps: CloudDefendPluginStartDeps; params: AppMountParameters; } -export const TestProvider: React.FC> = ({ +export const TestProvider: React.FC> = ({ core = coreMock.createStart(), deps = { data: dataPluginMock.createStartContract(), diff --git a/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts b/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts index 85ec23aff2a22..ae9866b4f03ca 100644 --- a/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts +++ b/x-pack/plugins/cloud_defend/server/lib/fleet_util.ts @@ -104,7 +104,7 @@ export const getInstalledPolicyTemplates = async ( perPage: 1000, }); - // getting installed policy templates by findings enabled inputs + // getting installed policy templates const enabledPolicyTemplates = queryResult.items .map((policy) => { return policy.inputs.find((input) => input.enabled)?.policy_template; diff --git a/x-pack/plugins/cloud_defend/server/routes/status/status.ts b/x-pack/plugins/cloud_defend/server/routes/status/status.ts index 74a43a8c2a6dd..5d6854a94cddf 100644 --- a/x-pack/plugins/cloud_defend/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_defend/server/routes/status/status.ts @@ -78,7 +78,7 @@ const calculateCloudDefendStatusCode = ( if (timeSinceInstallationInMinutes <= INDEX_TIMEOUT_IN_MINUTES) return 'indexing'; if (timeSinceInstallationInMinutes > INDEX_TIMEOUT_IN_MINUTES) return 'index-timeout'; - throw new Error('Could not determine csp status'); + throw new Error('Could not determine cloud defend status'); }; const assertResponse = ( @@ -169,19 +169,11 @@ const getCloudDefendStatus = async ({ return response; }; -export const statusQueryParamsSchema = schema.object({ - /** - * CSP Plugin initialization includes creating indices/transforms/tasks. - * Prior to this initialization, the plugin is not ready to index findings. - */ - check: schema.oneOf([schema.literal('all'), schema.literal('init')], { defaultValue: 'all' }), -}); - export const defineGetCloudDefendStatusRoute = (router: CloudDefendRouter): void => router.get( { path: STATUS_ROUTE_PATH, - validate: { query: statusQueryParamsSchema }, + validate: {}, options: { tags: ['access:cloud-defend-read'], }, @@ -189,13 +181,6 @@ export const defineGetCloudDefendStatusRoute = (router: CloudDefendRouter): void async (context, request, response) => { const cloudDefendContext = await context.cloudDefend; try { - /* if (request.query.check === 'init') { - return response.ok({ - body: { - isPluginInitialized: cloudDefendContext.isPluginInitialized(), - }, - }); - }*/ const status = await getCloudDefendStatus(cloudDefendContext); return response.ok({ body: status, From daeca167e2edc3254732e3b79a0453eea5037d91 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 21 Feb 2023 22:14:48 +0000 Subject: [PATCH 14/19] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- .../cloud_defend/public/common/api/use_setup_status_api.ts | 4 ++-- x-pack/plugins/cloud_defend/server/routes/status/status.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts index e62bc058f22db..2d07d138e8d92 100644 --- a/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts +++ b/x-pack/plugins/cloud_defend/public/common/api/use_setup_status_api.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { useKibana } from '../hooks/use_kibana'; import { CloudDefendSetupStatus } from '../../../common/types'; import { STATUS_ROUTE_PATH } from '../../../common/constants'; @@ -16,6 +16,6 @@ export const useCloudDefendSetupStatusApi = () => { const { http } = useKibana().services; return useQuery( [getCloudDefendSetupStatusQueryKey], - () => http.get(STATUS_ROUTE_PATH), + () => http.get(STATUS_ROUTE_PATH) ); }; diff --git a/x-pack/plugins/cloud_defend/server/routes/status/status.ts b/x-pack/plugins/cloud_defend/server/routes/status/status.ts index 5d6854a94cddf..0283843254978 100644 --- a/x-pack/plugins/cloud_defend/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_defend/server/routes/status/status.ts @@ -10,7 +10,6 @@ import type { SavedObjectsClientContract, Logger } from '@kbn/core/server'; import type { AgentPolicyServiceInterface, AgentService } from '@kbn/fleet-plugin/server'; import moment from 'moment'; import { PackagePolicy } from '@kbn/fleet-plugin/common'; -import { schema } from '@kbn/config-schema'; import { ALERTS_INDEX_PATTERN, INTEGRATION_PACKAGE_NAME, From d9223e65da84babca5f449d6f54af63bc0fa82bb Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Tue, 21 Feb 2023 22:36:39 +0000 Subject: [PATCH 15/19] further cleanup --- .../public/application/router.tsx | 1 - .../public/components/loading_state/index.tsx | 7 +++--- .../cloud_defend/server/routes/index.ts | 24 ------------------- 3 files changed, 4 insertions(+), 28 deletions(-) delete mode 100644 x-pack/plugins/cloud_defend/server/routes/index.ts diff --git a/x-pack/plugins/cloud_defend/public/application/router.tsx b/x-pack/plugins/cloud_defend/public/application/router.tsx index 42010ea047ca5..3fa261d35c765 100644 --- a/x-pack/plugins/cloud_defend/public/application/router.tsx +++ b/x-pack/plugins/cloud_defend/public/application/router.tsx @@ -19,7 +19,6 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } }, }); -/** Props for the cloud_defend router component */ export interface CloudDefendRouterProps { securitySolutionContext?: CloudDefendSecuritySolutionContext; } diff --git a/x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx b/x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx index bee83f2d563b8..608eabf0e30a8 100644 --- a/x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx +++ b/x-pack/plugins/cloud_defend/public/components/loading_state/index.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiLoadingSpinner, EuiSpacer, EuiPageTemplate } from '@elastic/eui'; +import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { FullSizeCenteredPage } from '../full_size_page'; // Keep this component lean as it is part of the main app bundle export const LoadingState: React.FunctionComponent<{ ['data-test-subj']?: string }> = ({ @@ -14,10 +15,10 @@ export const LoadingState: React.FunctionComponent<{ ['data-test-subj']?: string ...rest }) => { return ( - + {children} - + ); }; diff --git a/x-pack/plugins/cloud_defend/server/routes/index.ts b/x-pack/plugins/cloud_defend/server/routes/index.ts deleted file mode 100644 index 1a5467577aa30..0000000000000 --- a/x-pack/plugins/cloud_defend/server/routes/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IRouter, Logger } from '@kbn/core/server'; - -export function defineRoutes(router: IRouter, logger: Logger) { - router.get( - { - path: '/api/cloud_defend/example', - validate: false, - }, - async (context, request, response) => { - return response.ok({ - body: { - time: new Date().toISOString(), - }, - }); - } - ); -} From f0ad5f45a9eca4ff25ef4ebadb006c88f78f89e4 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Thu, 23 Feb 2023 19:42:38 +0000 Subject: [PATCH 16/19] removed commented out code --- .../security_solution/public/cloud_defend/links.ts | 11 ----------- .../security_solution/public/management/links.ts | 5 +---- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/plugins/security_solution/public/cloud_defend/links.ts index 80b1ba83c2033..d921c23d4b236 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_defend/links.ts @@ -30,14 +30,3 @@ export const manageLinks: LinkItem = { landingIcon: IconCloudDefend, ...commonLinkProperties, }; - -// currently using the CSP category, as it's weird to have two categories each with one item.. -// saving this for when we add other pages -/* export const manageCategories: LinkCategories = [ - { - label: i18n.translate('xpack.securitySolution.appLinks.category.cloudDefend', { - defaultMessage: 'DEFEND FOR CONTAINERS (D4C)', - }), - linkIds: [SecurityPageName.cloudDefendPolicies], - }, -]; */ diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 0d644922da21e..57f2c53a7be19 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -49,10 +49,7 @@ import { manageCategories as cloudSecurityPostureCategories, manageLinks as cloudSecurityPostureLinks, } from '../cloud_security_posture/links'; -import { - // manageCategories as cloudDefendCategories, - manageLinks as cloudDefendLinks, -} from '../cloud_defend/links'; +import { manageLinks as cloudDefendLinks } from '../cloud_defend/links'; import { IconActionHistory } from './icons/action_history'; import { IconBlocklist } from './icons/blocklist'; import { IconEndpoints } from './icons/endpoints'; From 7e67cc9c2a0bc2c1407c27858e86e70571d8634f Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Mon, 27 Feb 2023 21:44:27 +0000 Subject: [PATCH 17/19] tsconfig fix --- x-pack/plugins/cloud_defend/tsconfig.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/cloud_defend/tsconfig.json b/x-pack/plugins/cloud_defend/tsconfig.json index 6bcf3e6b6325c..17d92a825c62a 100755 --- a/x-pack/plugins/cloud_defend/tsconfig.json +++ b/x-pack/plugins/cloud_defend/tsconfig.json @@ -7,7 +7,6 @@ "common/**/*", "public/**/*", "../../../typings/**/*", - "server/**/*.json", "public/**/*.json" ], "kbn_references": [ @@ -30,7 +29,5 @@ "@kbn/securitysolution-es-utils", "@kbn/es-types" ], - "exclude": [ - "target/**/*" - ] + "exclude": ["target/**/*"] } From 7aaf6352b43d47c9b7532c7ad582724f20ba7679 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Tue, 28 Feb 2023 00:06:45 +0000 Subject: [PATCH 18/19] tsconfig fix --- x-pack/plugins/cloud_defend/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_defend/tsconfig.json b/x-pack/plugins/cloud_defend/tsconfig.json index 17d92a825c62a..d12ee82da6fce 100755 --- a/x-pack/plugins/cloud_defend/tsconfig.json +++ b/x-pack/plugins/cloud_defend/tsconfig.json @@ -6,8 +6,10 @@ "include": [ "common/**/*", "public/**/*", + "server/**/*", "../../../typings/**/*", - "public/**/*.json" + "public/**/*.json", + "server/**/*.json" ], "kbn_references": [ "@kbn/core", From fe3ec0406ad6ae4ef3a92af8886e2779337812c9 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Tue, 28 Feb 2023 17:12:12 +0000 Subject: [PATCH 19/19] clean up, per PR comments --- .../security_solution/public/cloud_defend/links.ts | 6 ------ .../security_solution/public/cloud_defend/routes.tsx | 9 +++------ .../security_solution/public/common/links/app_links.ts | 2 -- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cloud_defend/links.ts b/x-pack/plugins/security_solution/public/cloud_defend/links.ts index d921c23d4b236..652ebe6181151 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/links.ts +++ b/x-pack/plugins/security_solution/public/cloud_defend/links.ts @@ -16,12 +16,6 @@ const commonLinkProperties: Partial = { capabilities: [`${SERVER_APP_ID}.show`], }; -export const rootLinks: LinkItem = { - ...getSecuritySolutionLink('policies'), - globalNavPosition: 3, - ...commonLinkProperties, -}; - export const manageLinks: LinkItem = { ...getSecuritySolutionLink('policies'), description: i18n.translate('xpack.securitySolution.appLinks.cloudDefendPoliciesDescription', { diff --git a/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx b/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx index 4afbc47030810..18bd7641addf3 100644 --- a/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx +++ b/x-pack/plugins/security_solution/public/cloud_defend/routes.tsx @@ -11,7 +11,6 @@ import type { CloudDefendSecuritySolutionContext, } from '@kbn/cloud-defend-plugin/public'; import { CLOUD_DEFEND_BASE_PATH } from '@kbn/cloud-defend-plugin/public'; -import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import type { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; import { useKibana } from '../common/lib/kibana'; import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper'; @@ -35,11 +34,9 @@ const CloudDefend = () => { return ( - - - - - + + + ); }; diff --git a/x-pack/plugins/security_solution/public/common/links/app_links.ts b/x-pack/plugins/security_solution/public/common/links/app_links.ts index 719dc819c812c..c00fa87f878da 100644 --- a/x-pack/plugins/security_solution/public/common/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/common/links/app_links.ts @@ -14,7 +14,6 @@ import { links as managementLinks, getManagementFilteredLinks } from '../../mana import { dashboardsLandingLinks, threatHuntingLandingLinks } from '../../landing_pages/links'; import { gettingStartedLinks } from '../../overview/links'; import { rootLinks as cloudSecurityPostureRootLinks } from '../../cloud_security_posture/links'; -import { rootLinks as cloudDefendRootLinks } from '../../cloud_defend/links'; import type { StartPlugins } from '../../types'; const casesLinks = getCasesLinkItems(); @@ -22,7 +21,6 @@ const casesLinks = getCasesLinkItems(); export const links = Object.freeze([ dashboardsLandingLinks, detectionLinks, - cloudDefendRootLinks, cloudSecurityPostureRootLinks, timelinesLinks, casesLinks,